【译】继承与原型链(Inheritance and the prototype chain)

前言

原文来自MDN JavaScript主题的高阶教程部分,一共5篇。分别涉及继承与原型、严格模式、类型数组、内存管理、并发模型和事件循环。本篇是第一部分,关于继承和原型。javascript

原文连接请点我

下面是正文部分:java

对于熟悉基于类的编程语言(例如 Java 和 C++)的开发者来讲,JavaScript 会让他们感到困惑,由于 JS 的动态性以及其自己并不提供class的实现(ES2015 中提出的class关键字仅仅是语法糖,JS 仍然是基于原型的)git

提到继承,JavaScript 只有一个结构:对象(objects)。每一个对象都有一个私有属性,该属性连接到另外一个对象(称为该对象的原型(prototype))。这个原型对象自身也有一个原型,直到一个对象的原型为null。根据定义,null不存在原型,它表明这条原型链的终点。github

在 JavaScript 中,几乎全部对象都是Object的实例,Object在原型链顶端。web

尽管这种困惑常常被认为是 JavaScript 的缺点,可是这种原型式的继承模型实际上比一些经典的模型更为强大。例如,在一个原型式模型的基础上再构造一个经典模型是很是简单的。chrome


经过原型链继承

继承属性

JavaScript 对象就像一堆属性的动态“包裹”(这堆属性称为对象自身属性)(译者注:原文为 JavaScript objects are dynamic "bags" of properties (referred to as own properties).)。
JavaScript 对象有一个指向原型对象的连接。当访问一个对象的属性时,不只会在该对象上查找,还会在该对象的原型,以及这个原型的原型上查找,直到匹配上这个属性名或者遍历完该原型链。编程

根据 ECMAScript 标准, someObject.[[Prototype]]用于指定 someObject的原型。从 ECMAScript 2015 开始, [[Prototype]]能够经过 Object.getPrototypeOf()Object.setPrototypeOf()访问。这和经过 JavaScript 中的 __proto__访问是同样的,尽管这不标准,可是已经被不少浏览器所实现。
最好不要和函数的 _func_.prototype属性混淆。当一个函数被当作构造器(constructor)调用时,会生成一个对象,而函数上的 _func_.prototype属性引用的对象会做为生成对象的 [[Prototype]]存在。 Object.prototype就表示了 Object这一函数的 prototype。

下面例子展现了访问对象属性的过程:数组

// 让咱们使用构造函数f建立一个对象o,o上面有属性a和b:
let f = function () {
  this.a = 1;
  this.b = 2;
};
let o = new f(); // {a: 1, b: 2}

// 在f的prototype对象上添加一些属性
f.prototype.b = 3;
f.prototype.c = 4;

// 不要对prototype从新赋值好比: f.prototype = {b:3,c:4}; 这会打断原型链
// o.[[Prototype]] 上有属性b和c
// o.[[Prototype]].[[Prototype]] 就是 Object.prototype
// 最终, o.[[Prototype]].[[Prototype]].[[Prototype]] 为 null
// 这就是原型链的终端, 等于 null,
// 根据定义, null再也不有 [[Prototype]]
// 所以, 整条原型链看起来相似:
// {a: 1, b: 2} ---> {b: 3, c: 4} ---> Object.prototype ---> null

console.log(o.a); // 1
// o上存在自身属性'a'吗?固然,该属性值为1

console.log(o.b); // 2
// o上存在自身属性'b'吗?固然,该属性值为2
// prototype 上也有属性'b', 可是并不会被访问到
// 这叫作“属性覆盖”

console.log(o.c); // 4
// o上存在自身属性'c'吗?不存在, 继续查找它的原型
// o.[[Prototype]]上存在自身属性'c'吗?固然,该属性值为4

console.log(o.d); // undefined
// o上存在自身属性'd'吗?不存在, 继续查找它的原型
// o.[[Prototype]]上存在自身属性'd'吗?不存在, 继续查找o.[[Prototype]]的原型
// o.[[Prototype]].[[Prototype]] 为 Object.prototype, 上面不存在属性'd', 继续查找o.[[Prototype]].[[Prototype]]的原型
// o.[[Prototype]].[[Prototype]].[[Prototype]] 为 null, 中止查找
// 没找到属性'd',返回undefined

在线代码连接浏览器

在一个对象上设置属性称为建立了一个”自身属性“(译者注:原文为Setting a property to an object creates an own property.)。惟一会影响属性 set 和 get 行为的是当该属性使用getter 或者 setter定义。安全

继承“方法”

JavaScript 中并无像在基于类语言中定义的”方法“。在 JavaScript 中,任何函数也是以属性的形式被添加到对象中,继承的函数和其余继承的属性同样,也存在上面提到的”属性覆盖”(这里叫作方法覆盖(_method overriding_))。

当一个继承的函数被执行时,函数内的this指向当前继承的对象,而不必定是将该函数做为“自身属性“的对象自己。

var o = {
  a: 2,
  m: function () {
    return this.a + 1;
  },
};

console.log(o.m()); // 3
// 当调用 o.m 时, 'this' 指向 o

var p = Object.create(o);
// p 是一个继承o的对象

p.a = 4; // 在p上建立一个'a'属性
console.log(p.m()); // 5
// 当调用 p.m 时, 'this' 指向 p.
// 因此当 p 从 o 上继承了方法 m时,
// 'this.a' 等于 p.a

在 JavaScript 中使用原型

让咱们更详细地来看看背后的原理。

在 JavaScript 中,正如上面提到,函数也能够拥有属性。全部函数都有一个特殊的属性prototype。请注意下面的代码是独立的(能够安全地假设网页中除了下面的代码就没有其余代码了)。为了更好的学习体验,很是推荐你打开浏览器的控制台,点击'console'标签,复制粘贴如下代码,点击 Enter/Return 键来执行它。(大多数浏览器的开发者工具(Developer Tools)中都包含控制台。详情请查看Firefox 开发者工具Chrome 开发者工具,以及Edge 开发者工具

function doSomething() {}
console.log(doSomething.prototype);
//  无论你如何声明函数,
//  JavaScript中的函数都有一个默认的
//  prototype 属性
//  (Ps: 这里有一个意外,箭头函数上没有默认的 prototype 属性)
var doSomething = function () {};
console.log(doSomething.prototype);

能够在 console 中看到,doSomething()有一个默认的prototype属性,打印的内容和下面相似:

{
    constructor: ƒ doSomething(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

若是咱们在doSomething()prototype上添加属性,以下:

function doSomething() {}
doSomething.prototype.foo = "bar";
console.log(doSomething.prototype);

结果为:

{
    foo: "bar",
    constructor: ƒ doSomething(),
    __proto__: {
        constructor: ƒ Object(),
        hasOwnProperty: ƒ hasOwnProperty(),
        isPrototypeOf: ƒ isPrototypeOf(),
        propertyIsEnumerable: ƒ propertyIsEnumerable(),
        toLocaleString: ƒ toLocaleString(),
        toString: ƒ toString(),
        valueOf: ƒ valueOf()
    }
}

如今咱们能够经过new操做符来基于这个 prototype 对象建立doSomething()的实例。使用new操做符调用函数只须要在调用前加上new前缀。这样该函数会返回其自身的一个实例对象。接着咱们即可以往该实例对象上添加属性:

function doSomething() {}
doSomething.prototype.foo = "bar"; // 往prototype上添加属性'foo'
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // 往实例对象上添加属性'prop'
console.log(doSomeInstancing);

打印结果和以下相似:

{
    prop: "some value",
    __proto__: {
        foo: "bar",
        constructor: ƒ doSomething(),
        __proto__: {
            constructor: ƒ Object(),
            hasOwnProperty: ƒ hasOwnProperty(),
            isPrototypeOf: ƒ isPrototypeOf(),
            propertyIsEnumerable: ƒ propertyIsEnumerable(),
            toLocaleString: ƒ toLocaleString(),
            toString: ƒ toString(),
            valueOf: ƒ valueOf()
        }
    }
}

能够得知,doSomeInstancing__proto__就是doSomething.prototype。可是,这表明什么呢?放你访问doSomeInstancing的一个属性时,浏览器会首先查看doSomeInstancing自身是否存在该属性。

若是不存在,浏览器会继续查找doSomeInstancing__proto__(或者说是 doSomething.prototype)。若是存在,则doSomeInstancing__proto__的这个属性会被使用。

不然,会继续查找doSomeInstancing__proto____proto__。默认状况下,任何函数 prototype 属性的__proto__属性就是window.Object.prototype。所以,会在doSomeInstancing__proto____proto__(或者说是doSomething.prototype.__proto__,或者说是Object.prototype)继续查找对应属性。

最终,直到全部的__proto__被查找完毕,浏览器会断言该属性不存在,所以得出结论:该属性的值为 undefined。

然咱们在 console 上再添加一些代码:

function doSomething() {}
doSomething.prototype.foo = "bar";
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value";
console.log("doSomeInstancing.prop:      " + doSomeInstancing.prop);
console.log("doSomeInstancing.foo:       " + doSomeInstancing.foo);
console.log("doSomething.prop:           " + doSomething.prop);
console.log("doSomething.foo:            " + doSomething.foo);
console.log("doSomething.prototype.prop: " + doSomething.prototype.prop);
console.log("doSomething.prototype.foo:  " + doSomething.prototype.foo);

结果以下:

doSomeInstancing.prop:      some value
doSomeInstancing.foo:       bar
doSomething.prop:           undefined
doSomething.foo:            undefined
doSomething.prototype.prop: undefined
doSomething.prototype.foo:  bar

使用不一样的方法建立对象和原型链

使用语法结构(字面量)建立对象

var o = { a: 1 };

// 新建立的对象以 Object.prototype 做为它的 [[Prototype]]
// o 没有叫作'hasOwnProperty'的自身属性
// hasOwnProperty 是 Object.prototype 的自身属性
// 也就是说 o 从Object.prototype 上继承了 hasOwnProperty
// Object.prototype 的原型为 null
// o ---> Object.prototype ---> null

var b = ["yo", "whadup", "?"];

// 数组继承自 Array.prototype
// (Array.prototype 上拥有方法例如 indexOf, forEach 等等)
// 原型链以下:
// b ---> Array.prototype ---> Object.prototype ---> null

function f() {
  return 2;
}

// 函数继承自 Function.prototype
// (Function.prototype 上拥有方法例如 call, bind, 等等)
// f ---> Function.prototype ---> Object.prototype ---> null

使用构造器函数

构造器函数和普通函数的差异就在于其刚好使用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 提出了一个新方法:Object.create()。调用该方法时会建立一个新对象。这个对象的原型为传入该函数的第一个参数:

var a = { a: 1 };
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承自 a )

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty);
// undefined, 由于 d 并无继承自 Object.prototype

Object.createnew操做符一块儿,使用delete操做符

下面的示例使用Object.create建立一个对象,并使用delete操做符来展现原型链的变化

var a = { a: 1 };

var b = Object.create(a);

console.log(a.a); // 1
console.log(b.a); // 1
b.a = 5;
console.log(a.a); // 1
console.log(b.a); // 5
delete b.a;
console.log(a.a); // 1
console.log(b.a); // 1(b.a 的值 5 已经被删除,所以展现其原型链上的值)
delete a.a; // 也可使用 'delete b.__proto__.a'
console.log(a.a); // undefined
console.log(b.a); // undefined

若是换成new操做符建立对象,原型链更短:

function Graph() {
  this.vertices = [4, 4];
}

var g = new Graph();
console.log(g.vertices); // print [4,4]
g.vertices = 25;
console.log(g.vertices); // print 25
delete g.vertices;
console.log(g.vertices); // print undefined

使用 class 关键字

ECMAScript 2015 提出了一系列新的关键字用于实现。包括classconstructorstaticextends以及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);

关于性能

若是须要查找的对象属性位于原型链的顶端,查找时间会对性能有影响,尤为对于对性能要求很高的应用来讲,影响会进一步放大。另外,若是是访问一个不存在的属性,老是会遍历整条原型链。

此外,当对对象的属性进行迭代查找时,原型链上全部可枚举的属性都会被遍历。为了检查哪些属性是对象的自身属性而不是来自其原型链,颇有必要使用继承自Object.prototypehasOwnProperty方法。下面来看一个具体的例子,该例子继续使用上一个图形的例子:

console.log(g.hasOwnProperty("vertices"));
// true

console.log(g.hasOwnProperty("nope"));
// false

console.log(g.hasOwnProperty("addVertex"));
// false

console.log(g.__proto__.hasOwnProperty("addVertex"));
// true

hasOwnProperty是 JavaScript 中查找对象属性时惟一不遍历原型链的方法。

注意:仅仅检查属性是undefined并不能表明该属性不存在,也许是由于它的值刚好被设置为了undefined

很差的实践:对原生的 prototypes 进行扩展

常常容易犯的一个错误是扩展Object.prototype或者是一些其余内置的 prototype。

这被称为是”猴子补丁“,会打破程序的封装性。尽管在一些出名的框架中也这样作,例如 Prototype.js,可是仍然没有理由在内置类型上添加非标准的功能。

扩展内置类型的惟一理由是保证一些早期 JavaScript 引擎的兼容性,例如Array.forEach(译者注:Array.forEach是在 ECMA-262-5 中提出,部分早期浏览器引擎没有实现该标准,所以须要 polyfill)

继承原型链的方法总结

下面表格展现了四种方法以及它们各自的优缺点。如下例子建立的inst对象彻底一致(所以控制台打印的结果也同样),除了它们之间有不一样的优缺点。

名称 举例 优势 缺点
使用new初始化 <pre lang="javascript"> function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = new foo; proto.bar_prop = "bar val"; bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop); </pre> 支持全部浏览器(甚至到IE 5.5),同时,运行速度、标准化以及JIT优化性都很是好 问题是,为了使用该方法函数必须被初始化。在初始化过程当中,构造函数可能会为每一个建立对象建立一些特有属性,然而例子中只会构造一次,所以这些特有信息只会生成一次,可能存致使潜在问题。 以外,构造函数初始化时可能会添加冗余的方法到实例对象上。不过,只要这是你本身的代码且你明确这是干什么的,这些一般来讲也不是问题(其实是利大于弊)。
使用Object.create <pre lang="javascript"> function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = Object.create( foo.prototype ); proto.bar_prop = "bar val"; bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop); </pre> <pre lang="javascript"> function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = Object.create( foo.prototype, { bar_prop: { value: "bar val" } } ); bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop) </pre> 支持目前全部的现代浏览器,包括非IE浏览器以及IE9及以上版本浏览器。至关于容许一次性设置proto,这样有利于浏览器优化该对象。同时也容许建立没有原型的对象例如:Object.create(null) 不支持IE8以及如下版本浏览器,不过,微软目前已再也不支持运行这些浏览器的操做系统,对大多数应用来讲这也不是一个问题。 以外,若是使用第二个参数,则对象的初始化会变慢,这也许会成为性能瓶颈,由于第二个参数做为对象描述符属性,每一个对象的描述符属性是另外一个对象。当以对象形式处理成千上万的对象描述符时,可能会严重影响运行速度。
使用Object.setPrototypeOf <pre lang="javascript"> function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = { bar_prop: "bar val" }; Object.setPrototypeOf( proto, foo.prototype ); bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop); </pre> <pre lang="javascript"> function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto; proto = Object.setPrototypeOf( { bar_prop: "bar val" }, foo.prototype ); bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop) </pre> 支持目前全部的现代浏览器,包括非IE浏览器以及IE9及以上版本浏览器。支持动态的操做对象的原型,甚至能够为Object.create(null)建立的对象强制添加一个原型 因为性能不佳,应该会被弃用。若是你敢在生产环境中使用这样的语法,JavaScript代码快速运行几乎不可能。由于许多浏览器优化了原型,举个例子,在访问一个对象上的属性以前,编译器会提早肯定原型上的属性在内存中的位置,可是若是使用了Object.setPrototypeOf对原型进行动态更改,这至关于扰乱了优化,甚至会让编译器从新编译并放弃对这部分的优化,仅仅是为了能让你这段代码跑起来。 同时,不支持IE8以及如下版本浏览器
使用proto <pre lang="javascript"> function foo(){} foo.prototype = { foo_prop: "foo val" }; function bar(){} var proto = { bar_prop: "bar val", __proto__: foo.prototype }; bar.prototype = proto; var inst = new bar; console.log(inst.foo_prop); console.log(inst.bar_prop); </pre> <pre lang="javascript"> var inst = { __proto__: { bar_prop: "bar val", __proto__: { foo_prop: "foo val", __proto__: Object.prototype } } }; console.log(inst.foo_prop); console.log(inst.bar_prop) </pre> 支持目前几乎全部的现代浏览器,包括非IE浏览器以及IE11及以上版本浏览器。将proto设置为非对象的类型不会抛出异常,可是会致使程序运行失败 严重过期并且性能不佳。若是你敢在生产环境中使用这样的语法,JavaScript代码快速运行几乎不可能。由于许多浏览器优化了原型,举个例子,在访问一个对象上的属性以前,编译器会提早肯定原型上的属性在内存中的位置,可是若是使用了proto对原型进行动态更改,这至关于扰乱了优化,甚至会让编译器从新编译并放弃对这部分的优化,仅仅是为了能让你这段代码跑起来。 同时,不支持IE10及如下版本浏览器。

prototypeObject.getPrototypeOf

对于从 Java 和 C++过来的开发者来讲,JavaScript 会让他们感到有些困惑,由于 JavaScript 是动态类型、代码无需编译能够在 JS Engine 直接运行(译者注:Java 代码须要编译成机器码后在 JVM 执行),同时它尚未类。全部的几乎都是实例(objects)。尽管模拟了class,但其本质仍是函数对象。

你也许注意到了function A上有一个特殊的属性prototype。这个特殊属性与 JavaScriptnew操做符一块儿使用。当使用new操做符建立出来一个实例对象,这个特殊属性prototype会被复制给该对象的内部[[Prototype]]属性。举个例子,当运行var a1 = new A()代码时,JavaScript(在内存中建立完新实例对象以后且准备运行函数A()以前,运行函数时函数内部的this会指向该对象)会设置:a1.[[Prototype]] = A.prototype
当你以后访问建立的对象属性时,JavaScript 首先会检查属性是否存在于对象自己,若是不存在,则继续查找其[[Prototype]]。这意味着你在prototype上定义的属性实际上被全部实例对象共享,若是你愿意,甚至能够修改prototype,这些改动会同步到全部存在的实例对象中。

若是在上面的例子中,你执行:var a1 = new A(); var a2 = new A();,那么a1.doSomething就是Object.getPrototypeOf(a1).doSomething,这和你定义的A.prototype.doSomething是同一个对象,因此:Object.getPrototypeOf(a1).doSomething === Object.getPrototypeOf(a2).doSomething === A.prototype.doSomething

简而言之,prototype是针对类型的,而Object.getPrototypeOf()对于实例对象是一致的。(译者注:原文为In short, prototype is for types, while Object.getPrototypeOf() is the same for instances.)。

[[Prototype]]会被递归地查找,例如:a1.doSomething, Object.getPrototypeOf(a1).doSomething, Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething等等,直到Object.getPrototypeOf返回null

所以,当你执行:

var o = new Foo();

其实是执行:

var o = new Object();
o[[Prototype]] = Foo.prototype;
Foo.call(o);

接着若是你访问:

o.someProp;

JavaScript 会检查是否 o 上存在自身属性someProp。若是不存在,继续检查Object.getPrototypeOf(o).someProp是否存在,若是还不存在继续检查Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp,依次类推。


总结

在编写基于原型的复杂代码以前,颇有必要先理解原型式的继承模型。同时,请注意代码中原型链的长度,而且在必要时将其分解以免可能存在的性能问题。此外,应该杜绝在原生的原型对象上进行扩展,除非是为了考虑兼容性,例如在老的 JavaScript 引擎上适配新的语言特性。


Tags: Advanced Guide Inheritance JavaScript OOP


本篇文章由一文多发平台ArtiPub自动发布

相关文章
相关标签/搜索