对于习惯了基于类的语言(C++或者Java)的开发者来说,通常对Javascript的原型继承感到困惑,因为Javascirpt不仅是动态语言,而且在ES2015之前没有提供class
关键字(ES2015的 class
只是一个语法糖,Javascript的原型继承依然保留)。
对于继承,Javascript的每个对象都有一个私有属性,这个私有属性指向一个被称作prototype
的原型对象,原型对象本身有自己的原型对象,以此类推,直到原型对象为 null
, null没有原型对象,并且作为这个prototype chain
(原型链)的终点。
用图示表示这个原型链为:
obj ---> 原型对象 ---> 原型对象的原型对象 ... ---> 原型对象n ---> null原型对象n的值为 null,并且作为这个原型链的终点
几乎所有对象都是Object
的实例, Object
处于原型链的顶端。
继承经常被看做是Javascript的弱点,但是事实上,原型继承模型本身比基于类的继承更强大,所以在原型之上构建class的模型
就显得有点微不足道。
原型链继承
继承属性
Javascript对象是一个动态的"属性包",Javascrit对象有一个指向原型对象的链接。当试图访问一个对象的属性时,这个属性不仅会在当前对象里查找,并且会在这个对象的原型对象上查找,以及原型对象的原型对象上查找,直到正确的属性被找到或者原型链到达终点。
根据ECMAScript标准,符号
someObject.[[Prototype]]
被用于指定为someObject
的原型对象。从ECMAScript 2015开始,[[Prototype]]
通过使用访问器Object.getPrototypeOf()
获取,通过Object.setPrototypeOf()
设置。这个方法和Javascript的私有属性__proto__
是等同的。(__proto__属性不是官方标准,但是大多数浏览器都有实现)不要对
func.prototype
属性有困惑,它用来为func
构造函数创建的对象指定原型对象[[Prototype]]
。Object.prototype
属性代表Object
的原型对象。
下面展示当访问一个属性时发生了什么:
// 首先用函数`f`创建一个对象let f = function () { this.a = 1; this.b = 2;}let o = new f() // {a: 1, b: 2}// 在函数f的原型对象上添加属性f.prototype.b = 3;f.prototype.c = 4// 不要用这种形式 f.prototype = {b:3, c: 4}来设置原型对象,它会破坏原型链;// o.[[Prototype]]有属性a和c// o.[[Prototype]].[[Prototype]]是Object.prototype// 最终,o.[[Prototype]].[[Prototype]].[[Prototype]]是null// 作为null没有[[Prototype]],这也是原型链的终点// 因此,完整的原型链是:// {a: 1, b: 2} ---> {b: 3, c: 4} ---> Object.prototype ---> nullconsole.log(o.a); // 1console.log(o.b); // 2// 原型对象也有一个属性`b`, 但是不可访问,这被称作`property shadowing`console.log(o.c); // 4// 在[[Prototype]]中有属性`c`console.log(o.d) // undefined// 在对象o以及原型链上找不到属性`d`, 所以返回undefined
继承方法
在Javascript里没有类方法, 任何函数都可以用属性的方式添加到对象里。一个被继承的方法和任何其他的属性行为保持一致,包括property shadowing
。
当继承的方法被执行时,this
指向继承对象,而不是定义方法所在的原型对象。
var o = { a: 2, m: function() { return this.a + 1; }};console.log(o.m()); // 3var p = Object.create(o);// p继承自op.a = 4;console.log(p.m()); // 5// 当p.m被调用时,`this`指向p// 所以当p从o继承方法m时,`this.a`等同于p.a, p的属性`a`
创建对象的几种方式以及不同的原形链
用字面量创建对象
var o = {a: 1};// 新创建的对象o有原型对象[[Prototype]], 即: `Object.prototype`// o从Object.prototype继承了`hasOwnProperty`方法// Object.prototype的原型对象为null// 完整的原型链为:// o ---> Object.prototype ---> nullvar b = ['yo', 'whadup', '?'];// 数组继承自Array.prototype// (它有方法 indexOf, forEach, etc)// 原型链为:// b ---> Array.prototype ---> Object.prototype ---> nullfunction f() { return 2;}// 函数继承自Function.prototype// (它有方法 call, bind, etc)// 原型链为:// f ---> Function.prototype ---> Object.prototype ---> null
通过构造函数创建对象
Javascript的构造函数只是一个通过运算符new
调用的函数:
function Graph() { this.vertices = []; this.edges = [];}Graph.prototype = { addVertex: function (v) { this.vertices.push(v); }};var g = new Graph();// g是一个对象,有自身属性`vertices`和`edges`.// 当new Graph()被调用后,g.[[Prototype]]是Graph.prototype
使用Object.create创建对象
ECMAScript 2015新增了一个方法: Object.create()
。调用这个方法可以创建一个新对象。这个对象的原型对象是这个函数的第一个参数:
var a = {a: 1};// a ---> Object.prototype ---> nullvar b = Object.create(a);// b ---> a ---> Object.prototype ---> nullconsole.log(b.a) // 1 (继承的属性)var c = Object.create(b)// c ---> b ---> a ---> Object.prototype ---> nullvar d = Object.create(null)// d ---> nullconsole.log(d.hasOwnProperty);// undefined, 因为d并没有继承自Object.prototype
使用class关键字创建对象
ECMAScript 2015 新增了class
关键字。虽然class构造函数和类很像,但是它们是不一样的。Javascript保留了基于原型的机制。新的关键字包括: class
, constructor
, static
, extends
, super
。
'use strict';class Polygon { constructor(height, width) { this.height = height; this.width = width; }}class Square extends Polygon { constructor(sideLength) { super(sideLength, sideLength); } get area() { return this.height * this.width; } set sideLength(newLength) { this.height = newLength; this.width = newLength; }}var square = new Square(2);
性能
原型链高频率查找属性对性能有负面影响,此外,访问不存在的属性会遍历整个原型链。
查找一个对象的属性时,在原型链中的所有可枚举的(enumerable
)属性都会被遍历。当检查一个对象自身是否包含某个属性而不是去它的原型链上查找时,必须使用 hasOwnProperty方法
。
function Graph() { this.vertices = []; this.edges = [];}Graph.prototype = { addVertex: function (v) { this.vertices.push(v); }};var g = new Graph();console.log(g.hasOwnProperty('vertices'));// trueconsole.log(g.hasOwnProperty('nope'));// falseconsole.log(g.hasOwnProperty('addVertex'));// falseconsole.log(g.__proto__.hasOwnProperty('addVertex'));// true
使用hasOwnProperty
方法是Javascript中唯一可以避免遍历整个原型链属性的途径。
注:只检查属性是否为undefined
是不够的,因为属性可能是存在的,但是它的值是 undefined