在基于类的语言中,对象是类的实例,并且类可以从另一个类继承。Javascript是一门基于对象的语言,这意味着对象直接从其他对象继承。
Javascript的原型存在诸多矛盾。它的某些复杂语法看起来像那些基于类的语言,这些语法问题掩盖了它的原型机制。它不直接让对象从其他对象继承,反而插入一个多余的间接层:通过构造函数产生对象。
——《Javascript语言精粹》
本文基本是用来解释这段话的,如果你看到这里就已经十分清楚,那大概就不需要往下看了。
如果你和曾经的我一样,迷失在原型、原型链、构造函数、constructor、prototype、__proto__、[[prototype]]这些名词中,希望本文能为你指引一二。
原型的定义
在MDN中,对继承与原型链的解释是这样的
当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object )都有一个私有属性(称之为[[prototype]])指向它的原型对象(prototype)。该原型对象也有一个自己的原型对象 ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
遵循ECMAScript标准,someObject.[[Prototype]] 符号是用于指向 someObject的原型。这个等同于 JavaScript 的非标准但许多浏览器实现的属性
__proto__。几乎所有 JavaScript 中的对象都是位于原型链顶端的Object的实例。
这条原型链构成了继承的基础 —— 对象可以通过原型链去访问原型对象的属性。
原型继承
其实js中原型继承的概念十分简单。
先构建一个有用的对象,然后以这个对象作为原型创建一个新的对象,新对象就继承了原有对象的属性。用Object.create()来构造的原型链就十分简单干净。
const animal = { a: 1 };
const dog = Object.create(animal);
console.log(dog, dog.a);
// {} 1
-
dog打印出来是一个空对象,但访问dog下面的a属性却能拿到值,展开对象之后会发现一个
__proto__属性指向animal。dog.a取值的时候,在dog的作用域中找不到a,然后会向上追溯原型链,追溯到animal有这个属性。 -
继续展开原型链,会发现这样一个结构 b => a => Object.prototype => null
-
Object 是一个构造方法,
ƒ Object() { [native code] },Object.prototype 才是原型对象。 -
JavaScript中的所有对象都来自Object;所有对象从Object.prototype继承方法和属性。 上面的
animal = { a: 1 }中,用对象字面量的方式创建了一个对象实例,它的原型就是Object.prototype。 -
根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
-
[[prototype]]属性是在对象创建时默认初始化的,回想几种对象的创建方式
- 对象字面量,生成了一个Object的实例,所以原型就是Object.prototype。
- Object.create(obj),原型是obj。如果想要一个没有原型的对象,Object.create(null)就好啦
- 构造函数,也就是new function生成一个对象,原型指向构造函数的prototype属性指向的那个对象(这个下面会展开)
- 类生成,new class生成一个对象,这个本质上也是构造函数。
伪类继承(其实还是原型继承)
我们常见的继承方式是构造函数和class关键字,它们实际上都是基于原型继承。class其实只是语法糖,js这种弱类型语言中并没有类这种结构。
构造函数定义
在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符来作用这个函数时,它就可以被称为构造方法(构造函数)。
理解原型对象与构造函数的关系
只要创建一个新函数,就会根据一组特定的规则为这个函数创建一个prototyoe属性,这个属性指向函数的原型对象。默认情况下,这个原型对象会constructor属性,这个属性是一个指回函数的指针。
在new一个构造函数生成一个实例时,实例的原型对象[[prototype]]会被初始化为构造函数的prototype属性指向的对象
function Graph() {};
console.log(Graph, Graph.prototype, Graph.prototype.constructor)
// ƒ
// {constructor: ƒ}
// ƒ Graph () {}
明白了这一点,就很好解释构造函数是怎样实现原型继承的了
function Graph() {
this.vertices = [];
this.edges = [];
}
Graph.prototype = {
addVertex: function(v){
this.vertices.push(v);
}
};
var g = new Graph();
// g是生成的对象,他的自身属性有'vertices'和'edges'.
// 在g被实例化时,g.[[Prototype]]指向了Graph.prototype.
上面说过,构造函数的prototype初始的时候指向一个带有constructor属性的对象,当对一个实例取constructor属性时,其实是取到了实例的原型对象上的constructor属性。就是这里将构造函数的prototype指向了一个新对象,就会产生一个不太算缺点的缺点——丢失了constructor属性。
解决方法也很简单, 加回去就好了
function Graph() {
this.vertices = [];
this.edges = [];
}
Graph.prototype = {
constructor: Graph.prototype.constructor,
addVertex: function(v){
this.vertices.push(v);
}
}
;
var g = new Graph();
console.log(g.constructor);
// Graph
构造函数实现继承的过程
// 构造函数Animal有一个默认的属性Animal.prototype,这个属性指向一个初始的原型对象,对象是Object的实例,对象的原型是Object.prototype.
function Animal() {}
// 构造函数Dog有一个默认的属性Dog.prototype,这个属性指向一个初始的原型对象,对象是Object的实例,对象的原型是Object.prototype.
function Dog() {}
// 构造函数Animal生成对象,对象原型为Animal.prototype
// 再将这个对象赋给Dog.prototype,作为Dog的原型
Dog.prototype = {
constructor: Dog.prototype.constructor,
...new Animal()
}
// 构造函数Dog将原型Dog.prototype赋给foo
const foo = new Dog();
如果只看原型链,是这样的
将构造函数考虑进来,就是这样
参考资料
- github.com/mqyqingfeng…
- developer.mozilla.org/zh-CN/docs/… 继承与原型链
- 《javascript高级程序设计》
- 《javascript语言精粹》