一篇搞懂 Javascript 原型、原型链

原型和原型链

JavaScript 中的对象有一个特殊的 [[Prototype]] 内置属性,就是对其他对象的引用,如果在对象上没有找到需要的属性或方法,就会继续在 [[Prototype]] 关联的对象上查找,层层向上找直到找到或者到对象原型为null时结束,这一系列 [[Prototype]] 链接就是原型链。

__proto__ 引用了 [[Prototype]],可以通过 __proto__.__proto__... 来遍历原型链。

所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype。

var obj = {}
Object.__proto__ === Object.prototype  // true
Object.getPrototypeOf(obj) === Object.prototype  // true
Object.getPrototypeOf(Object.prototype)  // null
Object.getPrototypeOf(Function.prototype) === Object.prototype  // true

当试图引用对象的属性时会触发 [[Get]] 操作,对于默认的 [[Get]],第一步是检查对象本身是否具有这个属性,如果无法在对象本身找到,就会继续访问对象的 [[Prototype]] 链。

var obj = {
    id: 1,
    name: 'xx'
}
// Object.create()会创建一个对象并把这个对象的 [[Prototype]] 关联到指定的对象
var obj2 = Object.create(obj) 
obj2 // {}
obj2.id // 1
obj2.name // 'xx'

以上例子,创建了一个关联到 obj 的对象 obj2,obj2 的 [[Prototype]] 指向 obj,obj2 本身是个空对象,访问 obj2.id 在 obj2 本身没有找到 id 这个属性,访问到了 [[Prototype]] 找到了这个属性。

属性设置和屏蔽

var obj = {
    a: 1
}
var _obj = Object.create(obj)

obj.b = 2  
// obj上没有b这个属性,则添加b,此时obj -> { a: 1, b: 2 }

_obj.a = 100
// _obj -> { a: 100 },obj -> { a: 1, b: 2 } 此为上图的1

Object.defineProperty(obj, 'b', {
  writable: false
})
// 将obj中的b设置为只读

_obj.b = 300
// _obj -> { a: 100 },此时不会在_obj上创建属性b

Object.defineProperty(_obj, 'b', { value: 200 })
// _obj -> { a: 100, b: 200 }

只读属性会阻止 [[Prototype]] 链下层隐式创建同名属性,这样做主要是为了模拟类属性的继承。但实际上并不会发生类似的继承复制,而且这个限制只存在于=赋值中,使 Object.defineProperty() 并不会受到影响。

JS中的‘类’

function Foo() {
  ...
}
var a = new Foo()
Object.getPrototypeOf(a) === Foo.prototype; // true
a.constructor == Foo // true

调用 new Foo() 时会创建 a,其中的一步就是给 a 一个内部的 [[Prototype]] 链接,关联到 Foo.prototype 指向的那个对象

  • js 中只有对象
  • js 中没有复制机制
  • 函数本身并不是构造函数,js 中的‘构造函数’可以理解为带 new 的函数调用

ES6 的 Class

ES6 的 Class 只是一个语法糖,是 ES5 的构造函数的一层包装

// es5
function es5Foo(x, y) {
	this.x = x;
	this.y = y;
}
es5Foo.prototype.add = function() {
	return this.x + this.y;
}
var f1 = new es5Foo(1, 2);

// es6
class es6Foo {
	constructor(x, y) {
		this.x = x;
		this.y = y;
	}
	add() {
		return this.x + this.y;
	}
}
var f2 = new es6Foo(1, 2);

类的所有方法都定义在类的 prototype 上,prototype 的 constructor 指向类本身,这与 ES5 一致

es5Foo.prototype.constructor === es5Foo  // true
Object.getPrototypeOf(f1) === es5Foo.prototype  // true

es6Foo.prototype.constructor === es6Foo  // true
Object.getPrototypeOf(f2) === es6Foo.prototype  // true

类的内部所有定义的方法都是不可枚举的,这与 ES5 不同

Object.keys(es5Foo.prototype)  // ['add']
Object.keys(es6Foo.prototype)  // []

new.target: ES6 为 new 命令引入了一个 new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数,如果构造函数不是通过 new 命令或 Reflect.construct() 调用的, new.target 会返回 undefined

  • 可以用来确保构造函数只能通过 new 调用
  • 可以用来写不能单独使用、必须继承后才能用的类

参考:Class 的基本语法

原型继承

js 中并没有复制机制,new 操作不是创建一个类的多个实例,而是可以创建多个对象并把它们的[[Prototype]] 关联到同一个对象,这个机制就是 js 中的原型继承,通过让一个对象的原型指向另一个对象实现继承

继承方式:

  • 原型链继承
  • 构造函数继承
  • 组合继承
  • 原型式继承
  • 寄生组合式继承

参考:MDN 继承和原型链