写在前面
注:这个系列是本人对js知识的一些梳理,其中不少内容来自书籍:Javascript高级程序设计第三版和JavaScript权威指南第六版,感谢它们的作者和译者。有发现什么问题的,欢迎留言指出。
起因
Object构造函数、对象字面量、Object.creat都可以用来创建单个对象,但有明显缺点:使用同一个接口创建很多对象,会产生大量的重复代码。所以才开始了创建对象的模式的探索。
检测对象的类
3种常见的检测任意对象的类的技术:instanceof运算符、constructor属性、构造函数的名字。3种各有优劣,适用于不同场景。(但往往我们更关注对象可以完成什么工作,对象属于哪个类并不是最重要的)
instanceof运算符
运算符左边是对象,右边是构造函数,如o instanceof f,如果在o的原型链中查找到f,就返回true:
var date = new Date();console.log(date instanceof Date);//trueconsole.log(date instanceof Object);//trueconsole.log(date instanceof Number);//false
这种方式的缺点:①无法通过对象来获得类名,只能检测对象是否属于指定的类名,②如果是不同的执行上下文,如客户端中每个窗口和框架子页面都具有单独的执行上下文,其中一个框架页面中的数组不是另一个框架页面的Array()构造函数的实例。
- constructor属性
console.log(date.constructor == Date);//true
缺点:①和instance的第2点缺点一样,执行上下文的问题,②并不是所有的对象都有constrctor属性,如果创建对象时把对象的prototype直接覆盖了而又没有指定constrctor属性,就会没有这个属性。
- 构造函数的名称
//返回对象的类function classof(o) { return Object.prototype.toString.call(o).slice(8,-1);}//返回函数的名字(可能是空字符串),不是函数就返回nullFunction.prototype.getName = function () { if("name" in this) return this.name; return this.name = this.toString().match(/function\s*([^()]*)\(/)[1];}function type(o) { //type,class,name var t,c,n; //处理null值特殊情况 if(o === null) return 'null'; //处理NaN和它自身不相等 if(o !== o) return "nan"; //识别出原始值的类型 if((t = typeof o) !== "object") return t; //识别出大多数的内置对象(类名除了"Object") if((c=classof(o)) !== "Object") return c; //如果对象构造函数的名字存在,就返回它 if(o.constructor && typeof o.constructor === "function" && (n = o.constructor.getName()) ) return n; //其他的类型无法判别,返回"Object" return "Object";}
这种方法的问题:①并不是所有的构造函数都有constructor属性,②并不是所有的函数都有名字(name是非标准属性),如果使用不带名字的函数定义表达式定义一个构造函数,getName()方法会返回空字符串。
//这种情况下如果没有name属性,那么getName()方法就返回空字符串了var Example = function (x, y) { this.x = x;this.y = y;}
1.工厂模式
function creatPerson(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function () { console.log(this.name); }; return o;}var person1 = creatPerson('jaychou',34,'singer');var person2 = creatPerson('xiaoming',15,'student');//{name: "jaychou", age: 34, job: "singer", sayName: ƒ}console.log(person1);//{name: "xiaoming", age: 15, job: "student", sayName: ƒ}console.log(person2);
工厂模式的最大缺点:没有解决对象识别的问题,不知道一个对象的类型。
2.构造函数模式
显然构造函数可用来创建特定类型的对象,如Array,Date等,重写上面的例子:
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function () { console.log(this.name); }}//{name: "jaychou", age: 34, job: "singer", sayName: ƒ}var person1 = new Person('jaychou',34,'singer');//{name: "xiaoming", age: 15, job: "student", sayName: ƒ}var person2 = new Person('xiaoming',15,'student');
与之前对比:①没有显式地创建对象,②直接将属性和方法赋给了 this 对象,③没有 return 语句
使用new操作符调用Person构造函数后:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(this指向了这个新对象)
- 执行构造函数中的代码(为新对象添加属性和方法)
- 返回新对象
类型的标识有了,构造函数模式的主要缺点是:每个方法都要在每个实例上重新创建一遍(函数也是对象的一种,导致重复创建对象了)。
3.原型模式
- 基本
function Person() {}Person.prototype.name = "jaychou";Person.prototype.age = 34;Person.prototype.job = 'singer';Person.prototype.sayName = function () { console.log(this.name);}var person1 = new Person();person1.sayName();//jaychouvar person2 = new Person();person2.sayName();//jaychouconsole.log(person1.sayName == person2.sayName);//true
创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
//{name: "jaychou", age: 34, job: "singer", sayName: ƒ, constructor: ƒ}console.log(Person.prototype);
我们打印了Person.prototype,默认情况下会有一个constructor属性,这个属性指向函数(在这里就是指向构造函数),其他的name,age,job,sayName属性和方法都是通过Person.prototype.添加进去的,所以由构造函数Person创建的实例都会包含这些属性和方法,它们是共享的。**
而且实例的内部包含一个指针(内部属性),指向构造函数的原型对象:[[Prototype]],在Firefox、Safari和Chrome中支持属性__proto__:
//{name: "jaychou", age: 34, job: "singer", sayName: ƒ, constructor: ƒ}console.log(person1.__proto__);console.log(Person.prototype == person1.__proto__);//true//打印对象的constructor:在这里person1的constructor就是Personconsole.log(person1.constructor == Person);//true
另外,可以通过 isPrototypeOf 方法来确定对象之间是否存在原型关系:
console.log(Person.prototype.isPrototypeOf(person1));//true
还有,可以通过 Object.getPrototype 返回对象的原型:
//打印对象的原型:{name: "jaychou", age: 34, job: "singer", sayName: ƒ, constructor: ƒ}console.log(Object.getPrototypeOf(person1));console.log(Object.getPrototypeOf(person1).name);//jaychou
之前也有提及,查询属性和方法时先在当前实例中找,没有的话就到实例的原型链中找,而在实例中添加的属性会屏蔽原型中的同名属性。
- 更简单的原型语法
上一个例子中每增加一个属性和方法就要敲一遍Person.prototype,比较麻烦,所以更简单的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:
Person.prototype = { name:'jaychou', age:34, job:'singer', sayName:function () { console.log(this.name); }}var person3 = new Person();console.log(Person.prototype.constructor == Person);//falseconsole.log(Person.prototype.constructor == Object);//trueconsole.log(person3.constructor == Person);//falseconsole.log(person3.constructor == Object);//true
上面的代码将 Person.prototype设置为等于一个以对象直接量形式创建的新对象,结果就导致了constructor 属性不再指向 Person 了,因为本质上完全重写了默认的prototype对象,因此constructor 属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向 Person函数。看下面的例子就更清楚了:
function Teacher(){};var tea1 = new Teacher();Person.prototype = tea1;//true:因为Person.prototype被重写成tea1,tea1的constructor自然指向了Teacher构造函数console.log(Person.prototype.constructor == Teacher);
所以,如果constrctor的值很重要,可以在上面代码的基础上手动加回去,最好仿照原生的把constructor属性设置成不可枚举的:
Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person});console.log(Person.prototype.constructor == Person);//true
注意一个问题: 如果某个实例已经被创建了之后,再直接重写了构造函数的原型对象,那么之前已被创建好的对象内部的原型指针还是指向旧的原型,如果旧实例调用了新原型里面定义的方法,就会报错了。所以重写函数的原型对象时要特别注意这个问题。
- 原型对象模式的问题
以上的创建对象在原型里共享了所有的属性和方法,对于方法还好,对于大多数情况下属性共享带来的问题就显而易见了。
4.组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job;}Person.prototype = { constructor:Person, sayName:function () { console.log(this.name); }}var person1 = new Person('jaychou',34,'singer');var person2 = new Person('xiaoming',15,'student');person1.sayName();//jaychouperson2.sayName();//xiaomingconsole.log(person1.sayName === person2.sayName);//true
5.动态原型模式(最常用)
对上面的例子进行视觉上的美化,希望把所有的内容都放在构造函数里面,可以通过检查某个应该存在的方法或属性是否存在,来决定是否需要初始化原型:
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; //添加共享的方法或属性 if(typeof this.sayName != "function"){ Person.prototype.sayName = function () { console.log(this.name); }; Person.prototype.sayJob = function () { console.log(this.job); } }}
注意: 使用动态原型模式时,不能使用对象直接量重写原型,原因上面已经解释过了,重写会切断了旧实例和新原型之间的联系。