红皮书(Javascript高级程序设计)的第6章是关于面向对象中的构造函数的内容,看了第二遍,对一些重点做下笔记。
构造器模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
}
}
var person1 = new Person("johnson", "22", "frontend engineer")
按照惯例,构造函数的函数名总是以大写字母开头,非构造函数的普通函数则是以小写字母开头,我想这也是为了做个明显的区分。
使用new 函数名()的形式的操作就是调用构造函数,而那些没有new 操作符调用的就是普通调用
函数在ECMAScript中就是对象, 每当一个函数被定义,其实就是一个对象被初始化。如下代码:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)"); //逻辑上等于 function() { alert(this.name) }
}
原型模式
每个函数被创建时都会有个原型属性,这个原型其实也就是个包含属性和方法的对象,同时也能被特殊引用类型的实例给访问。这个对象(prototype)也就是原型在构造器(constructor)被调用时就被创建了。
function Person() {}
Person.prototype.name = "Johnson";
Person.prototype.age = "22";
Person.prototype.job = "frontend engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //"Johnson"
var person2 = new Person();
person2.sayName(); //"Johnson"
console.log(person1.sayName === person2.sayName); //true
所有的原型自动会得到一个叫构造器(constructor)的属性并且这个属性指向这个原型在的这个函数。Person.prototype.constructor指向Person。
当定义一个自定义的构造器,原型就默认获得了constructor属性。其他的方法比如函数的toString方法就从Object继承(又是原型链)。每当用new操作符定义个新的实例(比如new Person()), 这个实例有一个内部指针指向这个构造器的原型。在ECMA-262第五版,也叫作[[prototype]]。如下图:
当一个属性在对象上能够被读取,有一种搜索规则来找到这个属性。这个搜索最开始从对象自身开始搜索,如果找到这个属性名这个属性值就被返回了并且不会继续往原型上找,当在对象自身不存在这个属性,则会到原型对象上去找这个属性,找到就返回。
如果一个属性被加到对象实例上(带new 操作符+函数名的实例)它会屏蔽原型上的同名属性,也意味着它锁住了访问原型同名属性的通道。即使用set方法把这个属性设置成Null也只是设置在对象本身的属性,并没有恢复访问原型同名属性的通道。除非用delete方法删除这个实例上的属性才能够重新访问到原型上这个同名属性的值。
function Person() {}
Person.prototype.name = "Johnson";
Person.prototype.age = "22";
Person.prototype.job = "frontend engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Jonas";
console.log(person1.name); //Jonas
console.log(person2.name); //Johnson
delete person1.name;
console.log(person1.name); //Johnson
遍历对象的属性
- for in
- Object.keys()
- Object.getOwnPropertyNames()
三者区别:
- for in能够输出自身以及原型链上可枚举的属性
var parent = Object.create(Object.prototype, {
a: {
value: 1,
writable: true,
enumerable: true,
configurable: true
}
});
var child = Object.create(parent, {
b: {
value: 2,
writable: true,
enumerable: true,
configurable: true
},
c: {
value: 3,
writable: true,
enumerable: false,
configurable: true
}
});
for (var key in child) {
console.log(key); // b a
}
- Object.keys()主要用来获取对象自身可枚举的属性键,返回的是键名的数组。
function Person() {}
Person.prototype.name = 'Johnson';
Person.prototype.age = 29;
Person.prototype.job = "frontend engineer";
Person.prototype.sayName = function() {
console.log(this.name);
}
var keys = Object.keys(Person.prototype); //此处遍历Person的原型对象
console.log(keys); //"name, age, job,sayName" 输出原型对象的key
var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
console.log(p1keys); //"name, age" 此处只输出自身可枚举属性,不输出原型的属性
- Object.getOwnPropertyNames()用来获取对象自身的全部属性,无论枚举不可枚举。
var keys = Object.getOwnPropertyNames(person.prototype);
console.log(keys); //"constructor(不可枚举), name, age, job, sayName"
交替的原型句法
function Person() {}
Person.prototype = {
name: "Johnson",
age: 29,
job: "frontend engineer",
sayName: function() {
console.log(this.name);
}
}
var friend = new Person();
console.log(friend instanceof Object); //true
console.log(friend instanceof Person); //true
console.log(friend.constructor == Person); //false
console.log(friend.constructor == Object); //true
以上的复写Person原型对象等同于创建了一个新的对象。但是构造器(constructor)属性不再指向Person。按照前面说的,当一个函数被创建时,它的原型对象会被创建并且构造器属性会被自动分配到原型对象中。但以上的写法完全复写了默认的原型对象,意味着构造器属性等同于在一个新的对象中,而不是函数的原型对象中。(翻译过来有点拗口,可以这么理解,原本这个构造函数是指向函数本身的,复写之后指向的是全局Object)。
function Person() {}
Person.prototype = {
constructor: Person,
name: "Johnson",
age: 29,
job: "frontend engineer",
sayName: function() {
console.log(this.name);
}
}
但我们也能够让这个原型对象中的constructor重新指回Person。但是记住以这种方式回复构造器(constructor)属性的指向会使得constructor属性变为可枚举(Enumerable)。原生的constructor属性默认是不可枚举的,所以你最好重新把它定义成不可枚举,这里要用到Object.defineProperty()方法:
function Person() {}
Person.prototype = {
constructor: Person,
name: "Johnson",
age: 29,
job: "frontend engineer",
sayName: function() {
console.log(this.name);
}
}
var p1 = new Person();
for(var i in p1) {
console.log(i); // constructor, name, age, job, sayName
}
//ECMAScript 5 only - restore the constructor
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
});
var p2 = new Person();
for(var j in p2) {
console.log(j); //name, age, job, sayName
}
原型的动态特质
function Person() {}
var friend = new Person();
Person.prototype.sayHi = function() {
console.log('hi');
}
friend.sayHi(); //hi
基于之前在原型上查找属性的惯例,在任何地方对原型做出修改都能直接反映到实例上,即使实例化操作在原型修改之前。也是因为实例和原型之间的连接只是个简单的指针而不是拷贝,当sayHi()被调用时,实例上是第一次搜索,没搜到,就继续到原型上去搜索直到搜索到方法并返回。
[[Prototype]]指针在构造器被调用时被分配到,所以修改原型变成一个不同的对象会切断构造器和原始的原型对象之间的联系。要记住一点:** 实例有一个只指向原型的指针而不是构造器 **,思考如下:
function Person() {}
var friend = new Person();
Person.prototype = {
constructor: Person,
name: 'Johnson',
age: 29,
job: 'Frontend engineer',
sayName: function() {
console.log(this.name);
}
};
friend.sayName(); //error
当friend.sayName()被调用时,会发生错误,因为friend指向的原型没有这个属性。如下图:
![U%6}O8T5]W5SC{WQ9YJ_C53.png](http://upload-images.jianshu.io/upload_images/1572265-592c3bfcbd49fa44.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
个人理解,实例化在重写原型之前,切断了实例和之前那个原型的连接,但看图来说,我认为是实例还是指向之前的原型,只不过函数本身指向了新的原型对象。**注意:重写原型在实例化之前,friend.sayName()方法还是执行的 **
寄生构造器模式
function Person(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 friend = new Person('johnson', 29, 'Frontend engineer');
friend.sayName(); //johnson
console.log(friend.name); //johnson
console.log(friend instanceof Person); //false
寄生模式下,friend能够访问到函数内部对象的属性。只是构造器和实例之间没有任何联系,所以instanceof不生效。
耐用模式
function Person(name, age, job) {
var o = new Object();
o.sayName = function() {
console.log(name);
}
return o;
}
var friend = Person('johnson', 29, 'Frontend engineer');
friend.sayName(); //johnson
原型链
一个简单的例子:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
//inherit from SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
return this.subproperty;
};
var instance = new SubType();
console.log(instance.getSuperValue());
值得注意的是,getSuperValue()方法依旧在SuperType.prototype对象上而property在SubType.prototype对象上。SubType的原型现在是SuperType的实例,所以属性property存在SubType的原型上。同时也注意instance.constructor指向SuperType,因为在SubType.prototype上的构造器属性被复写了。
调用instance.getSuperValue()经过3个步骤的搜索:
- 实例instance
- SubType.prototype
- SuperType.prototype
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
//inherit from SuperType
SubType.prototype = new SuperType();
var SuperTp = new SuperType();
console.log(SuperTp.getSuperValue()); //true
SubType.prototype.getSubValue = function() {
return this.subproperty;
};
//override existing method
SubType.prototype.getSuperValue = function() {
return false;
}
var instance = new SubType();
console.log(instance.getSuperValue()); //false
getSuperValue()方法被复写,原型上的的getSuperValue()方法会被遮蔽。当getSuperValue()在SubType实例上被调用,会调用复写的方法,但SuperType的实例还是会调用原型链上的方法。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
//inherit from SuperType
SubType.prototype = new SuperType();
SubType.prototype = {
getSubValue: function() {
return this.subproperty;
},
someOtherMethod: function() {
return false;
}
}
var instance = new SubType();
console.log(instance.getSuperValue()); //error
SubType的原型被复写成一个新的对象实例,所以原型链断开,SubType和SuperType现在没任何联系。
构造器偷取
function SuperType() {
this.colors = ['red', 'yellow'];
}
SuperType.prototype.sayColor = function() {
console.log('1111');
};
function SubType() {
SuperType.call(this);
}
var instance = new SubType();
instance.colors.push('black');
console.log(instance.colors);// ['red', 'yellow', 'black']
instance.sayColor() //error
var instance2 = new SubType();
console.log(instance2.colors); //['red', 'yellow']
** 记住函数只是在一个特殊上下文中执行代码的简单对象。**
apply和call方法能够被用作执行一个构造器在一个新创建的对象上。当构造器上的属性包含引用时不会造成共享,各个实例有自己的属性引用。好比以上的属性的数组。此模式还支持传递参数。
缺点:缺点在以上代码也很明显,就是被继承的函数上的原型方法不能够被继承的调用。
混合继承
混合继承结合了原型链和构造器偷取的优点。
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age) {
//inherit properties
SuperType.call(this, name);
this.age = age;
}
//inherit methods
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
}
var instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); //red, blue, green, black
instance1.sayName(); //Nicholas
instance1.sayAge(); //29
var instance2 = new SubType('Greg', 27);
console.log(instance2.colors); //red, blue, green
instance2.sayName(); //Greg
instance2.sayAge(); //27
这就解决了刚才构造器偷取不能访问被继承函数的原型方法的问题。并且最后的实例不会互相影响。
原型继承
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
console.log(person.friends); //[ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]
新对象将person对象作为自己的原型。person.friends不仅被person共享而且也被anotherPerson和yetAnotherPerson共享。这个代码有两个person的克隆。
var person = {
name: 'Nicholas',
friends: ['Shelly', 'Court', 'Van']
};
var anotherPerson = Object.create(person, {
name: {
value: 'Greg'
}
});
anotherPerson.friends.push('Rob');
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
console.log(anotherPerson.name); //Greg
console.log(person.friends); //Shelly, Court, Van, Rob, Barbie
Object.create()方法 第一个参数作为原型对象,第二个参数则是附加的属性,最后返回一个新对象。记住按照这种模式属性中只要包含引用值,将会共享这个引用值,类似于原型方式。使用这种方法你不用去创建各种构造器。
寄生模式
function createAnother(original) {
var clone = object(original);
clone.sayHi = function() {
console.log('hi);
};
return clone;
}
var person = {
name: 'Nicholas',
friends: ['Shelly', 'Court', 'Van']
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //hi
这种模式虽然能在无构造器和自定义类型下实现但也不是很高效。
最后我们来说一下:
寄生混合模式
此方法是对之前** 混合继承 **的一种改善,因为混合继承会调用2次superType的构造器。一次是创建subType.prototype,另一次是在subtype的构造器中。
当SubType构造器被调用,SuperType的构造器也被调用了。你会发现,有两个name和colors的集合属性,一个在instance实例上一个在SubType的原型上。
解决调用两次构造器的基本思想是代替调用supertype的构造器来分配给subType的原型。你只需要一个supetype的原型拷贝。
function inheritProtoype(subType, superType) {
var prototype = Object(superType.prototype); //create object
prototype.constructor = subType; //augment obect
subType.prototype = prototype; //assign object
}
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
}
var instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); //red, blue, green, black
instance1.sayName(); //Nicholas
instance1.sayAge(); //29
var instance2 = new SubType('Greg', 27);
console.log(instance2.colors); //red, blue, green
instance2.sayName(); //Greg
instance2.sayAge(); //27
寄生混合模式还是很高效的。