对采用过如前所述类的词汇 (如 Java 或 C++) 的开发者而言,JavaScript 有点儿自相矛盾,即使它是静态的,因此这类不提供更多两个 class 同时实现。(在 ES2015/ES6 中导入了 class URL,但那而已句法糖,JavaScript 依然是如前所述蓝本的)。
当谈及承继时,JavaScript 多于一类内部结构:第一类。每一示例第一类( object )都有两个专有特性(称作 __proto__ )对准它的缺省的蓝本第一类(prototype )。该蓝本第一类也有两个他们的蓝本第一类( __proto__ ) ,一层层向下直至两个第一类的蓝本第一类为 null。依照表述,null 没蓝本,并做为那个蓝本链中的最终两个各个环节。
基本上大部份 JavaScript 中的第一类都是坐落于蓝本链顶部的 Object 的示例。
虽然此种蓝本承继一般而言被指出是 JavaScript 的软肋众所周知,但蓝本承继数学模型这类事实上比经典之作数学模型更强悍。比如,在蓝本数学模型的基础上构筑经典之作数学模型十分单纯。
如前所述蓝本链的承继
承继特性
JavaScript 第一类是静态的特性“包”(指其他们的特性)。JavaScript 第一类有两个对准两个蓝本第一类的链。当试图出访两个第一类的特性时,它更为重要在该第一类上追踪,还会追踪该第一类的蓝本,和该第一类的蓝本的蓝本,依序一层层向下搜寻,直至找出两个英文名字相匹配的特性或抵达蓝本链的结尾。
遵从ECMAScript国际标准,someObject.[[Prototype]] 记号是用作对准 someObject 的蓝本。从 ECMAScript 6 已经开始,[[Prototype]] 能透过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 出访器来出访。那个等同 JavaScript 的冗余但很多应用程序同时实现的特性 __proto__。
但它不应该与缺省 func 的 prototype特性相混淆。被缺省创建的示例第一类的[[Prototype]] 对准 func 的 prototype 特性。Object.prototype特性表示 Object 的蓝本第一类。
这里演示当尝试出访特性时会发生什么:
// 让我们从两个函数里创建两个第一类o,它自身拥有特性a和b的: let f =function () { this.a = 1; this.b = 2; } /* 这么写也一样function f() { this.a = 1; this.b = 2; } */ let o = new f(); // {a: 1, b: 2} // 在f函数的蓝本上表述特性 f.prototype.b =3; f.prototype.c = 4; // 不要在 f 函数的蓝本上直接表述 f.prototype = {b:3,c:4};这样会直接打破蓝本链 // o.[[Prototype]]有特性 b 和 c // (其实就是 o.__proto__ 或者 o.constructor.prototype) // 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// a是o的自身特性吗?是的,该特性的值为1 console.log(o.b); // 2 // b是o的自身特性吗?是的,该特性的值为 2 // 蓝本上也有两个b特性,但它不会被出访到。 // 此种情况被称为“特性遮蔽 (property shadowing)” console.log(o.c); // 4 // c是o的自身特性吗?不是,那看看它的蓝本上有没 // c是o.[[Prototype]]的特性吗?是的,该特性的值为4 console.log(o.d); // undefined // d 是 o 的自身特性吗?不是,那看看它的蓝本上有没 // d 是 o.[[Prototype]]的特性吗?不是,那看看它的蓝本上有没 // o.[[Prototype]].[[Prototype]] 为 null,停止搜寻 // 找不到 d 特性,返回 undefinedps://repl.it/@khaled_hossain_code/prototype
承继方法
JavaScript 并没其他如前所述类的词汇所表述的“方法”。在 JavaScript 里,任何函数都能添加到第一类上做为第一类的特性。函数的承继与其他的特性承继没差别,包括上面的“特性遮蔽”(此种情况相当于其他词汇的方法重写)。
当承继的函数被调用时,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,就是 p 的自身特性 a在 JavaScript 中采用蓝本
接下去,来仔细分析一下这些应用场景下, JavaScript 在背后做了哪些事情。
正如之前提到的,在 JavaScript 中,函数(function)是允许拥有特性的。大部份的函数会有两个特别的特性 ——prototype。请注意,以下的代码是独立的(出于严谨,假定页面没其他的JavaScript代码)。为了最佳的学习体验,我们强烈建议阁下打开应用程序的控制台(在Chrome和火狐应用程序中,按Ctrl+Shift+I即可),进入“console”选项卡,然后把如下的JavaScript代码复制粘贴到窗口中,最终透过按下回车键运行代码。
function doSomething(){} console.log( doSomething.prototype ); // 和声明函数的方式无关, // JavaScript 中的函数永远有两个默认蓝本特性。 var doSomething = function(){}; console.log( doSomething.prototype );在控制台显示的JavaScript代码块中,我们能看到doSomething函数的两个默认特性prototype。而这段代码运行之后,控制台应该显示类似如下的结果:
{ constructor: ƒ doSomething(), __proto__: { constructor: ƒObject(), hasOwnProperty: ƒ hasOwnProperty(), isPrototypeOf: ƒ isPrototypeOf(), propertyIsEnumerable: ƒ propertyIsEnumerable(), toLocaleString: ƒ toLocaleString(), toString: ƒtoString(), valueOf: ƒ valueOf() } }我们能给doSomething函数的蓝本第一类添加新特性,如下:
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操作符来创建如前所述那个蓝本第一类的doSomething示例。采用new操作符,只需在调用doSomething函数语句之前添加new。这样,便能获得那个函数的两个示例第一类。一些特性就能添加到该蓝本第一类中。
请尝试运行以下代码:
function doSomething(){} doSomething.prototype.foo =“bar”; // add a property onto the prototype var doSomeInstancing = newdoSomething(); doSomeInstancing.prop =“some value”; // add a property onto the object 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 不包含特性信息, 那么应用程序会在 doSomeInstancing 的 __proto__ 中进行查找(同 doSomething.prototype). 如特性在 doSomeInstancing 的 __proto__ 中查找出,则采用 doSomeInstancing 中 __proto__ 的特性。
否则,如果 doSomeInstancing 中 __proto__ 不具有该特性,则检查doSomeInstancing 的 __proto__ 的 __proto__ 是否具有该特性。默认情况下,任何函数的蓝本特性 __proto__ 都是 window.Object.prototype. 因此, 透过doSomeInstancing 的 __proto__ 的 __proto__ ( 同 doSomething.prototype 的 __proto__ (同 Object.prototype)) 来查找要搜寻的特性。
如果特性不存在doSomeInstancing 的 __proto__ 的 __proto__ 中, 那么就会在doSomeInstancing 的 __proto__ 的 __proto__ 的 __proto__中查找。然而, 这里存在个问题:doSomeInstancing 的 __proto__ 的 __proto__ 的 __proto__ 其实不存在。因此,多于这样,在 __proto__的整个蓝本链被查看之后,这里没更多的__proto__ , 应用程序断言该特性不存在,并给出特性值为 undefined 的结论。
让我们在控制台窗口中输入更多的代码,如下:
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}; // o 那个第一类承继了 Object.prototype 上面的大部份特性 // o 自身没名为 hasOwnProperty 的特性 // hasOwnProperty 是 Object.prototype 的特性 // 因此 o 承继了 Object.prototype 的 hasOwnProperty // Object.prototype 的蓝本为 null // 蓝本链如下: // o —> Object.prototype —> null var a = [“yo”, “whadup”, “?”]; // 数组都承继于 Array.prototype // (Array.prototype 中包含 indexOf, forEach 等方法) // 蓝本链如下: // a —> Array.prototype —> Object.prototype —> null function f(){ return 2; }// 函数都承继于 Function.prototype // (Function.prototype 中包含 call, bind等方法) // 蓝本链如下: // f —> Function.prototype —> Object.prototype —> null采用构造器创建的第一类
在 JavaScript 中,构造器其实就是两个普通的函数。当采用 new 操作符 来作用那个函数时,它就能被称为构造方法(缺省)。
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。采用 Object.create 创建的第一类
ECMAScript 5 中导入了两个新方法:Object.create()。能调用那个方法来创建两个新第一类。新第一类的蓝本就是调用 create 方法时传入的第两个参数:
var a = {a: 1}; // a —> Object.prototype —> null var b = Object.create(a); // b —> a —> Object.prototype —> null console.log(b.a);// 1 (承继而来) 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采用 class URL创建的第一类
ECMAScript6 导入了一套新的URL用来同时实现 class。采用如前所述类词汇的开发者会对这些内部结构感到熟悉,但它们是不同的。JavaScript 依然如前所述蓝本。这些新的URL包括class, constructor,static,extends 和 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; } setsideLength(newLength) {this.height = newLength; this.width = newLength; } } var square = new Square(2);性能
在蓝本链上查找特性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,企图出访不存在的特性时会遍历整个蓝本链。
遍历第一类的特性时,蓝本链上的每一可枚举特性都会被枚举出来。要检查第一类是否具有他们表述的特性,而不是其蓝本链上的某个特性,则必须采用大部份第一类从Object.prototype 承继的 hasOwnProperty 方法。下面给出两个具体的例子而言明它:
console.log(g.hasOwnProperty(vertices)); // true console.log(g.hasOwnProperty(nope)); // false console.log(g.hasOwnProperty(addVertex)); // false console.log(g.__proto__.hasOwnProperty(addVertex)); // truehasOwnProperty是 JavaScript 中唯一两个处理特性因此不会遍历蓝本链的方法。(译者注:原文如此。另一类这样的方法:Object.keys())
注意:检查特性是否为 undefined 是不能够检查其是否存在的。该特性可能已存在,但其值恰好被设置成了undefined。
错误实践:扩展原生第一类的蓝本
经常采用的两个错误实践是扩展 Object.prototype 或其他内置蓝本。
此种技术被称为猴子补丁因此会破坏封装。虽然一些流行的框架(如 Prototype.js)在采用该技术,但依然没足够好的理由采用附加的冗余方法来混入内置蓝本。
扩展内置蓝本的唯一理由是支持 JavaScript 引擎的新特性,如 Array.forEach。
总结:4 个用作拓展蓝本链的方法
下面列举四种用作拓展蓝本链的方法,和他们的优势和缺陷。下列四个例子都创建了完全相同的 inst 第一类(所以在控制台上的输出也是一致的),为了举例,唯一的区别是他们的创建方法不同。
prototype 和 Object.getPrototypeOf
对从 Java 或 C++ 转过来的开发者而言,JavaScript 会有点儿让人困惑,即使它完全是静态的,都是运行时,而且不存在类(class)。大部份的都是示例(第一类)。即使我们模拟出的 “类”,也而已两个函数第一类。
你可能已经注意到我们的 function A 有两个叫做 prototype的特殊特性。该特殊特性可与 JavaScript 的new 操作符一起采用。对蓝本第一类的引用被复制到新示例的内部 [[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(补充:事实上,执行 a1.doSomething() 十分于执行 Object.getPrototypeOf(a1).doSomething.call(a1)==A.prototype.doSomething.call(a1))
简而言之, prototype 是用作类的,而 Object.getPrototypeOf() 是用作示例的(instances),两者功能一致。
[[Prototype]] 看起来就像递归引用, 如 a1.doSomething、Object.getPrototypeOf(a1).doSomething、Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething 等等等, 直至它被找出或 Object.getPrototypeOf 返回 null。
因此,当你执行:
var o = new Foo();JavaScript 事实上执行的是:
var o = new Object(); o.__proto__ = Foo.prototype; Foo.call(o);(或者类似上面这样的),然后,当你执行:
o.someProp;它检查 o 是否具有 someProp 特性。如果没,它会查找 Object.getPrototypeOf(o).someProp,如果仍旧没,它会继续查找 Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp。
结论
在采用蓝本承继编写复杂代码之前,理解蓝本承继数学模型是至关重要的。此外,请注意代码中蓝本链的长度,并在必要时将其分解,以避免可能的性能问题。此外,原生蓝本不应该被扩展,除非它是为了与新的 JavaScript 特性兼容。
译者注:在英文原版中,以下内容已被移除。保留仅作参考。
示例
B 承继自 A:
function A(a){ this.varA = a; } // 以上函数 A 的表述中,既然 A.prototype.varA 总是会被 this.varA 遮蔽, // 那么将 varA 加入到蓝本(prototype)中的目的是什么? A.prototype = { varA : null, /* 既然它没任何作用,干嘛不将 varA 从蓝本(prototype)去掉 ? 也许做为一类在隐藏类中优化分配空间的考虑 ? https://developers.google.com/speed/articles/optimizing-javascript 如果varA并不是在每一示例中都被初始化,那这样做将是有效果的。 */ doSomething : function(){ // … } } function B(a, b){ A.call(this, a); this.varB = b; } B.prototype = Object.create(A.prototype, { varB : { value: null, enumerable: true, configurable: true, writable: true }, doSomething : { value: function(){ // override A.prototype.doSomething.apply(this, arguments); // call super // … }, enumerable: true, configurable: true, writable: true } }); B.prototype.constructor = B; var b = newB(); b.doSomething();给个谢谢
Java并发编程之计划任务ScheduledExecutor
Java加密与解密之对称加密DES