JavaScript 常被描述为一种基于原型的图解语言 ——每个对象拥有一个原型对象,对象以其原型为模板、原型从原型继承方法和属性。和原原型对象也可能拥有原型,型链并从中继承方法和属性,图解一层一层、原型以此类推。和原这种关系常被称为原型链 (prototype chain) ,型链它解释了为何一个对象会拥有定义在其他对象中的图解属性和方法。更准确地说,原型这些属性和方法定义在对象的和原构造器函数的prototype属性上,而非对象实例本身。型链 在传统的图解 OOP 中,首先定义“类”,原型然后创建对象实例时,和原类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。 那么,到底什么是原型呢?让我们从对象的服务器托管属性开始讲起! 在JavaScript世界中,万物皆对象,可以说,JS对象是动态的属性“包”(指自己的属性)。 // 从一个函数里创建一个对象 f,它自身拥有属性 a 和 b : let Foo = function () { this.a = 1; this.b = 2; this.doSomthng = function() { console.log(hello); } } /* 这么写也一样 function Foo() { this.a = 1; this.b = 2; this.doSomthng = function() { console.log(hello, f); } } */ let f = new Foo(); // { a: 1, b : 2 } // 我们也可以为函数添加属性 Foo.c = 3 Foo.doSomething = function() { console.log(hello, Foo); 如何能知道对象有哪些属性呢?后续写一篇专门讨论对象的属性,这里我们用Object.getOwnPropertyNames来获取对象自身的属性: // 一个对象自己的属性是指直接对该对象定义的属性,而不是从该对象的原型继承的属性。 // 对象的属性包括字段(对象)和函数。 // `Object.getOwnPropertyNames`方法同时返回可枚举的和不可枚举的属性和方法的名称。 Object.getOwnPropertyNames(f) // [a, b, doSomthng] 这里的c,doSomething是我们手动为Foo添加的,其余的是云服务器提供商Foo函数创建之初自带的属性。注意到Foo的属性中有一个prototype原型属性,这就是我们将要讨论到的函数原型对象。 在JavaScript中,每个实例对象都有一个私有属性(称之为[[Prototype]],等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__。函数(function)在JS中也是对象,是允许拥有属性的。所有的函数会有一个特别的属性 —— prototype 。继承的属性和方法是定义在 prototype 属性之上的。 function doSomething(){ } console.log( doSomething.prototype ); // 和声明函数的方式无关,JavaScript 中的函数永远有一个默认原型属性。 var doSomething = function(){ }; console.log( doSomething.prototype ); / { constructor: ƒ doSomething(), __proto__: { constructor: ƒ Object(), hasOwnProperty: ƒ hasOwnProperty(), isPrototypeOf: ƒ isPrototypeOf(), propertyIsEnumerable: ƒ propertyIsEnumerable(), toLocaleString: ƒ toLocaleString(), toString: ƒ toString(), valueOf: ƒ valueOf(), __proto__: null } } 我们可以给 doSomething 函数的原型对象添加新属性,并通过 new 操作符来创建基于这个原型对象的 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(), __proto__: null } } */ var doSomeInstancing = new doSomething(); doSomeInstancing.prop = "some value"; // 在实例对象中添加属性 console.log( doSomeInstancing ); console.log( doSomeInstancing.foo ); // bar / { prop: "some value", __proto__: { foo: "bar", constructor: ƒ doSomething(), __proto__: { constructor: ƒ Object(), hasOwnProperty: ƒ hasOwnProperty(), isPrototypeOf: ƒ isPrototypeOf(), propertyIsEnumerable: ƒ propertyIsEnumerable(), toLocaleString: ƒ toLocaleString(), toString: ƒ toString(), valueOf: ƒ valueOf(), __proto__: null } } } 如上所示,亿华云计算 doSomeInstancing 中的__proto__是 doSomething.prototype,而且doSomeInstancing继承到了doSomething的foo属性。 其实,每个实例对象的__proto__属性指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。JavaScript 中几乎所有对象都是位于原型链顶端的 Object 的实例。 为了更清晰地理解,这里将原型相关的内容画了一张图。图中从任意一个对象出发,都可以顺着原型属性 __proto__ 找到一条原型链,对象属性的查找正是遵循原型链的(下面会以constructor属性为例)。 // f --> Foo.prototype --> Object.prototype --> null f.__proto__ === Foo.prototype // true f.__proto__.__proto__ === Object.prototype // true f.__proto__.__proto__.__proto__ === null // true // F --> Function.prototype --> Object.prototype --> null F.__proto__ === Function.prototype // true F.__proto__.__proto__ === Object.prototype // true F.__proto__.__proto__.__proto__ === null // true // o --> Object.prototype --> null // true o.__proto__ === Object.prototype // true o.__proto__.__proto__ === null // true // 扩展一下: // Array --> Function.prototype --> Object.prototype --> null Array.__proto__ === Function.prototype // true Array.__proto__.__proto__ === Object.prototype // true 从上图我们可以得知,prototype对象有个constructor对象,指向原构造函数。函数的原型对象和函数本身通过prototype属性和constructor属性形成一个循环引用。 Foo.prototype.constructor === Foo // true Function.prototype.constructor === Function // true 既然原型对象有constructor属性,那普通对象有吗? Object.getOwnPropertyNames(Foo.prototype) // [constructor] Object.getOwnPropertyNames(f) // [a, b, doSomthng] f.constructor / ƒ () { this.a = 1; this.b = 2; this.doSomthng = function() { console.log(hello); } } 咋一看,普通对象 f 并没有自己的constructor属性,但是如果你尝试访问它,你可以得到一个结果,这不就是我们定义的函数 Foo!确实,f是Foo函数通过new构造出来的,但是f并没有属于自己的constructor属性,我们看到的结果是f从它的原型链中获取到的(这就是图中用虚线表示constructor关系的原因)。函数对象的constructor同理! f.constructor === f.__proto__.constructor // true f.constructor === Foo // true Object.constructor === Object.__proto__.constructor // true Object.constructor === Function // true Function.constructor === Function.__proto__.constructor // true JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾,直到最后还未找相应属性,则断言该属性不存在,并给出属性值为 undefined 的结论。 prototype 是用于类(函数)的,而 Object.getPrototypeOf() 是用于实例的(instances),两者功能一致。 function A() { } A.prototype.doSomething = function () { console.log(A) } a1 = new A() a1.doSomething() //执行 `a1.doSomething()` 相当于执行 A.prototype.doSomething.call(a1) instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。 function Foo() { } ; var f1 = new Foo(); 特别地: Object instanceof Object // true Function instanceof Function // true Object instanceof Function // true 因为: Object.__proto__.__proto__ === Object.prototype //true Function.__proto__ === Function.prototype //true Object.__proto__ === Function.prototype //true 在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。 要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty 方法。 let o = { a: 1}; // o 继承了 Object.prototype 上面的所有属性 // o 自身没有名为 hasOwnProperty 的属性 // hasOwnProperty 是 Object.prototype 的属性 // 因此 o 继承了 Object.prototype 的 hasOwnProperty // Object.prototype 的原型为 null // 原型链如下: // o ---> Object.prototype ---> null var a = [1, 2, 3]; // 数组都继承于 Array.prototype // (Array.prototype 中包含 indexOf, forEach 等方法) // 原型链如下: // a ---> Array.prototype ---> Object.prototype ---> null function f(){ return 2; } // 函数都继承于 Function.prototype // (Function.prototype 中包含 call, bind 等方法) // 原型链如下: 在 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。 new 关键字会进行如下的操作: 创建一个空的简单 JavaScript 对象(即{ });为步骤 1 新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;将步骤 1 新创建的对象作为this的上下文 ;如果该函数没有返回对象,则返回this。function myNew(Fn, ...args) { const obj = { } obj.__proto__ = Fn.prototype // const obj = Object.create(Fn.prototype); const res = Fn.apply(obj, args); return res instanceof Object ? res : obj 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 ECMAScript6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不同的。JavaScript 仍然基于原型。这些新的关键字包括 class, constructor, static, extends, super。class不过是ES6的语法糖罢了。 "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; } } 原型链的内容是比较难理解的,里面的概念容易混淆,牢记以下几点,对着前面的图边写代码边梳理关系,相信你也很快就能搞定JS的原型和原型链!原型和原型链
原型 prototype 和 __proto__
原型链 prototype chain
不同方式创建对象和生成原型链
1. 使用语法结构创建的对象
2. 使用构造器创建的对象
3. 使用 Object.create 创建的对象
4. 使用 class 关键字创建的对象
总结