前言

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

原文链接请点我

上面是注释局部:

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

提到继承,JavaScript 只有一个构造:对象(objects)。每个对象都有一个公有属性,该属性链接到另一个对象(称为该对象的原型(prototype))。这个原型对象本身也有一个原型,直到一个对象的原型为null。依据定义,null不存在原型,它代表这条原型链的起点。

在 JavaScript 中,简直所有对象都是Object的实例,Object在原型链顶端。

只管这种困惑常常被认为是 JavaScript 的毛病,然而这种原型式的继承模型实际上比一些经典的模型更为弱小。例如,在一个原型式模型的根底上再结构一个经典模型是非常简单的。


通过原型链继承

继承属性

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 ---> nullconsole.log(o.a); // 1// o上存在本身属性'a'吗?当然,该属性值为1console.log(o.b); // 2// o上存在本身属性'b'吗?当然,该属性值为2// prototype 上也有属性'b', 然而并不会被拜访到// 这叫做“属性笼罩”console.log(o.c); // 4// o上存在本身属性'c'吗?不存在, 持续查找它的原型// o.[[Prototype]]上存在本身属性'c'吗?当然,该属性值为4console.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' 指向 ovar 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 valuedoSomeInstancing.foo:       bardoSomething.prop:           undefineddoSomething.foo:            undefineddoSomething.prototype.prop: undefineddoSomething.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 ---> nullvar b = ["yo", "whadup", "?"];// 数组继承自 Array.prototype// (Array.prototype 上领有办法例如 indexOf, forEach 等等)// 原型链如下:// b ---> Array.prototype ---> Object.prototype ---> nullfunction 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 ---> nullvar b = Object.create(a);// b ---> a ---> Object.prototype ---> nullconsole.log(b.a); // 1 (继承自 a )var c = Object.create(b);// c ---> b ---> a ---> Object.prototype ---> nullvar d = Object.create(null);// d ---> nullconsole.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); // 1console.log(b.a); // 1b.a = 5;console.log(a.a); // 1console.log(b.a); // 5delete b.a;console.log(a.a); // 1console.log(b.a); // 1(b.a 的值 5 曾经被删除,因而展现其原型链上的值)delete a.a; // 也能够应用 'delete b.__proto__.a'console.log(a.a); // undefinedconsole.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 25delete 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"));// trueconsole.log(g.hasOwnProperty("nope"));// falseconsole.log(g.hasOwnProperty("addVertex"));// falseconsole.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主动公布