JavaScript 的原型继承

一张经常被使用的关系图

一张经常被使用的关系图

JavaScript 中的每个对象都有一个 [[Prototype]] 的内部属性,该属性指向的对象就是当前对象的原型。每个原型对象也有自己的原型,依次向上遍历形成原型链。当我们访问一个对象的属性或者方法时,如果在当前对象中没有找到该属性或者方法,会通过原型链依次从原型对象中查找,从而实现对象继承的功能。我们可以通过 __proto__ 属性或者 Object.getPrototypeOf() / Object.setPrototypeOf()方法来访问对象的原型:

javascriptlet obj = {};
obj.__proto__ == Object.getPrototypeOf(obj) // true

虽然 __proto__ 属性并不是 ECMAScript 标准的一部分,但是大部分 JavaScript 实现都提供了该方法。

JavaScript 通过 new Constructor() 语句创建对象,其中 Constructor 是对象的构造函数。JavaScript 标准内置对象,比如 ObjectFunctionBooleanNumberStringArray 等,都是函数类型:

javascripttypeof Object == 'function'   //true
typeof Function == 'function' //true
typeof Boolean == 'function'  //true
typeof Number == 'function'   //true
typeof String == 'function'   //true
typeof Array == 'function'    //true

在 JavaScript 中,函数类型同时也是对象,因此也能拥有属性和方法。即便我们没有在代码中显式声明,每个函数也都会有 prototype 属性。如果该属性指向一个对象,那么 new 操作符会将其设置为新对象的原型:

javascriptfunction Constructor() {}
let obj = new Constructor()
obj.__proto__ == Constructor.prototype // true

函数默认的 prototype 是一个包含 constructor 属性的对象,而 constructor 指向这个函数自身:

javascriptfunction Constructor() {}
Constructor.prototype.constructor == Constructor // true

而创建的对象通过原型链继承,可以访问到原型的 constructor 函数:

javascriptfunction Constructor() {}
let obj = new Constructor()
obj.constructor == Constructor // true

在运行时,我们可以通过对象的 constructor 属性来 new 一个同类型的对象:

javascriptfunction Constructor() {}
let obj1 = new Constructor()
let obj2 = new obj1.constructor()
obj2 instanceof Constructor // true

但是 JavaScript 并不保证原型对象存在 constructor 属性。如果显式指定构造函数的 prototype 属性或者对象的 __proto__ 属性,会覆盖原型默认的 constructor 属性,此时通过 obj.constructor 是无法获取到 obj 对象真正的构造函数的:

javascriptfunction Constructor() {}
let Prototype = {}
Constructor.prototype = Prototype
let obj = new Constructor()
(new obj.constructor) instanceof Constructor // false
obj.__proto__.__proto__.constructor == obj.constructor // true

此时的 obj.constructor 实际指向 Prototype.__proto__ 对象的 constructor

对象的原型可以在运行时被修改或替换,从而改变对象的行为:

javascriptfunction Constructor() {}
Constructor.prototype.method = function () { console.log('original') }

let obj = new Constructor()
obj.method() // 控制台打印 original

// 修改原型
Constructor.prototype.method = function () { console.log('override') }
obj.method() // 控制台打印 override

// 替换原型
obj.__proto__ = {
    method() { console.log('replace') }
}
obj.method() // 控制台打印 replace

不过运行时替换构造函数的 prototype 并不会影响到已经创建的对象行为:

javascriptfunction Constructor() {}
Constructor.prototype.method = function () { console.log('original') }

let obj = new Constructor()
obj.method() // 控制台打印 original

// 替换构造函数的 prototype
Constructor.prototype = {
    method () { console.log('replace') }
}
obj.method() // 控制台打印 original

总结:

  1. 所有对象都具有隐含的 [[Prototype]] 属性,指向当前对象的原型对象,可以通过 __proto__ 属性访问;
  2. 所有类型为 function 的对象默认具有 prototype 属性,可以是 null 或者指向一个原型对象;
  3. 假设有构造函数 function Constructor() {},则 Constructor.prototype 会被赋于 (new Constructor).__proto__
  4. 对象的 [[Prototype]] 属性以及构造函数的 prototype 属性允许在运行时修改;
  5. 替换构造函数的 prototype 属性不会对已经创建的对象产生影响。