原型链的2个重要点
-
__proto__ === constructor.prototype
; - 对象查找一个属性的时候,会首先检查自身是否有这个属性,如果没有,则沿着原型链(也就是
__proto__
)往上寻找,直到寻找到null
。
继承
继承是为了复用对象。下面是几种继承方法:
-
原型链继承
function A(name) { this.name = name; this.city = ['Guangzhou', 'Shenzhen']; }; A.prototype.getName = function () { return this.name }; function B(sex) { this.sex = sex; } B.prototype = new A('Bobo'); var c = new B('male'); c.getName(); // Bobo // 缺陷 c.city.push('Shanghai'); var d = new B('jojo'); d.city; // [ 'Guangzhou', 'Shenzhen', 'Shanghai' ]
解释:
根据
__proto__ === constructor.prototype
可以知道,子类实例(也就是c)可以通过__proto__
访问到子类(也就是B)的prototype
,由于B.prototype = new A('Bobo')
, 子类的实例能访问到父类(也就是A)的实例。这样,子类实例(c)就能访问到父类的私有方法,然后通过父类实例(也就是new A('Bobo')
或者B.prototype
或者c.__proto__
)的__proto__
(也就是new A('bobo').__ptoto__
或者B.prototype.__proto__
或者c.__proto__.__proto__
) 访问到父类的原型上的方法。子类继承父类的属性和方法是将父类的私有属性和公有方法都作为自己的公有属性的方法。如果私有属性是引用类型的,实例进行修改,将会导致下一个实例的对应的私有属性发生变化。
优势:可以函数共享。
缺陷:当上一个实例修改了原型中的
引用类型
的值,会影响到下一个实例。 -
构造函数继承
function A (name) { this.city = ['Guangzhou', 'Shenzhen']; this.name = name; } A.prototype.getName = function() { return this.name; }; function B(name, sex) { A.call(this, name); this.sex = sex; } var c = new B('coco', 'male'); // c.getName(); // 无法共享方法 c.name; // coco c.sex; // male c.city.push('Shanghai'); var d = new B('jojo', 'female'); d.city; // [ 'Guangzhou', 'Shenzhen' ]
解释:
为什么叫构造函数继承呢,是因为对于新new对象(比如例子的c)来说,B就是他的构造函数,在B内进行继承,所以叫构造函数继承。
构造函数继承,就是在子类函数里面使用
call
,apply
来改变this
指向,让它指向父类,改变上下文环境。由于父类方法是挂载在prototype
所以子类无法获取到挂载的方法。优势:属性是实例借助构造函数自己生成的,所以各个实例的属性是各自独立的。创建子类实例的时候,可以向父类传递参数,可以实现多重继承(call多个对象)
缺陷:实例是子类实例,不是父类的实例;无法继承父类原型的属性和方法;不能复用,因为每个子类都有父类实例函数的副本。
-
结合继承
function A (name) { this.city = ['Guangzhou', 'Shenzhen']; this.name = name; } A.prototype.getName = function() { return this.name; }; function B(name, sex) { A.call(this, name); // 调用了一次父类的构造函数,会被下次调用屏蔽 this.sex = sex; } B.prototype = new A(); // 又调用了一次父类的构造函数, A原型上的属性其实是没有必要的 // 可以用 B.prototype = A.prototype来避免2次调用父类构造函数, // 但是这样,造成了子类原型链上的混乱,无法知道实例是子类创造的还是父类创造的,它们指向同一个 // 可以用 Object.create(A.prototype) // B.prototype.constructor = B;来替代,不过这种方法可以参照圣杯模式 var c = new B('dodo', 'male'); c.getName(); // dodo c.city.push('Shanghai'); var d = new B('eoeo', 'female'); d.city; // [ 'Guangzhou', 'Shenzhen' ]
显而易见地,结合继承结合了原型继承和构造函数继承的优点。
缺陷:子类原型上有一份多余的父类实例属性,父类构造函数被调用了2次,生成了2份,子类实例上的那一份屏蔽了子类原型上的。
-
寄生结合继承(圣杯模式)
// 写法1 function inherit(c, p) { function f() {} f.prototype = p.prototype; c.prototype = new f(); c.prototype.constructor = c; // uber是超类,储存这个目标是继承于谁,可写可不写 c.prototype.uber = p.prototype; } // 写法2 var inherit2 = (function(c, p){ var F = function(){}; return function(c, p) { F.prototype = p.prototype; c.prototype = new F(); c.uber = p.prototype; c.prototype.constructor = c; } })(); function A (name) { this.city = ['Guangzhou', 'Shenzhen']; this.name = name; } A.prototype.getName = function() { return this.name; }; function B(name, sex) { this.sex = sex; this.name = name; } inheirt(B, A); var c = new B('fofo', 'male'); c.name; // fofo c.sex; // male // c.city; // 没有这个属性 c.getName(); // fofo
创建一个中间对象f,将父类的
prototype
指向中间对象f的prototype
;然后子类实现f的原型继承,再矫正子类的constructor
,这样不会造成原型链的混乱。从而隔开了子类修改原型对父类造成的影响。但是无法继承父类的属性(比如父类的city属性);关键点在于
__proto__===constructor.prototype
的理解。对象会循着__proto__
向上寻找属性,如果自身没有的话。这样当A.prototype = new B()
的时候,var c = new A()
当c
执行一个方法或者查找一个属性没有找到的时候,c.__proto__ -> A.prototype(new B()) -> A.prototype.__proto__ -> B.prototype -> ... -> null
; 所以这样是可以实现原型链查找,这也是继承的体现之处。