吃透 JavaScript 继承(面试必备)
原型继承
原型链是实现原型继承的主要方法,基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法。
实现原型链的基本模式
1 | function SuperType(){ |
例子中的实例及构造函数和原型之间的关系图:
在例子代码中,定义了两个对象,subType和superType。
1 | 两个对象之间实现了继承,而这种继承方式是通过创建SuperType的实例并将该实例赋给subType.prototype实现的。实现的本质就是重写了原型对象。 |
这样subType.prototype中就会存在一个指针指向superType的原型对象。也就是说,存在superType的实例中的属性和方法现在都存在于subType.prototype中了。这样继承了之后,又可以为subType添加新的方法和属性。
要注意,这个指针([[prototype]])默认情况下是不可以再被外部访问的,估计是会被一些内部方法使用的,例如用for…in来遍历原型链上可以被枚举的属性的时候,就需要通过这个指针找到当前对象所继承的对象。不过,Firefox、Safari和Chrome在每个对象上都支持一个属性__proto__。
原型继承需要注意的一些问题
1. 别忘记默认的类型
我们知道,所有的引用类型都继承了Object,而这个继承也是通过原型链实现的。所以所有的对象都拥有Object具有的一些默认的方法。如:hasOwnProperty()、propertyIsEnumerable()、toLocaleString()、toString()和valueOf()
2. 确定原型和实例的关系 可以通过两种方式来确定原型和实例之间的关系。
① 使用instanceof 操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。
② 第二种方式是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()方法也会返回true。
例子:
1 | alert(instance instanceof Object); //true |
③ 子类要在继承后定义新方法
因为,原型继承是实质上是重写原型对象。所以,如果在继承前就在子类的prototype上定义一些方法和属性。那么继承的时候,子类的这些属性和方法将会被覆盖。
④ 不能使用对象字面量创建原型方法
这个的原理跟第三点的实际上是一样的。当你使用对象字面量创建原型方法重写原型的时候,实质上相当于重写了原型链,所以原来的原型链就被切断了。
⑤ 注意父类包含引用类型的情况
这个例子中的SuperType 构造函数定义了一个colors 属性,该属性包含一个数组(引用类型值)。SuperType 的每个实例都会有各自包含自己数组的colors 属性。当SubType 通过原型链继承了SuperType 之后,SubType.prototype 就变成了SuperType 的一个实例,因此它也拥有了一个它自己的colors 属性——就跟专门创建了一个SubType.prototype.colors 属性一样。但结果是什么呢?结果是SubType 的所有实例都会共享这一个colors 属性。而我们对instance1.colors 的修改能够通过instance2.colors 反映出来。也就是说,这样的修改会影响各个实例。
原型继承的缺点(问题)
- 最明显的就是上述第⑤点,有引用类型的时候,各个实例对该引用的操作会影响其他实例。
- 没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
有鉴于此,实践中很少会单独使用原型继承。
借用构造函数继承
在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数 (constructor stealing)的技术(有时候也叫做伪造对象或经典继承)。这种技术的基本思想相当简单,即 在子类型构造函数的内部调用超类型构造函数。
基本模式
1 | function SuperType(){ |
基本思想
借用构造函数的基本思想就是利用call或者apply把父类中通过this指定的属性和方法复制(借用)到子类创建的实例中。因为this对象是在运行时基于函数的执行环境绑定的。也就是说,在全局中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。call 、apply方法可以用来代替另一个对象调用一个方法。call、apply 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象。
所以,这个借用构造函数就是,new对象的时候(注意,new操作符与直接调用是不同的,以函数的方式直接调用的时候,this指向window,new创建的时候,this指向创建的这个实例),创建了一个新的实例对象,并且执行SubType里面的代码,而SubType里面用call调用了SuperTyep,也就是说把this指向改成了指向新的实例,所以就会把SuperType里面的this相关属性和方法赋值到新的实例上,而不是赋值到SupType上面。所有实例中就拥有了父类定义的这些this的属性和方法。