7.1 理解原型
-
在JavaScript中,对象是属性名与属性值的集合。创建一个对象字面量。
let obj = { prop1: 1, prop2: function() { }, prop3: {}, }
-
对象属性可以是简单值(如数值、字符串)、函数或其他对象。
-
JavaScript是动态语言,可以修改或删除对象的属性。
obj.prop1 = 1; obj.prop1 = []; delete obj.prop2; obj.prop4 = 'Hello';
-
最终obj如下
{ prop1: [], prop3: {}, prop4: 'Hello', }
-
为了避免重复造轮子,希望可以尽可能地复用代码。继承是代码复用的一种方式,可以将一个对象的属性扩展到另一个对象上。在JavaScript中可以通过原型实现继承。
-
每个对象都含有原型的引用,当查找属性时,若对象本身不具有该属性,则会查找原型上是否有该属性。
const yoshi = { skulk: true }; const hatori = { sneak: true }; const kuma = { creep: true }; assert("skulk" in yoshi, "Yoshi can skulk"); assert(!("sneak" in yoshi), "Yoshi cannot sneak"); assert(!("creep" in yoshi), "Yoshi cannot creep"); Object.setPrototypeOf(yoshi, hatori); assert("sneak" in yoshi, "Yoshi can now sneak"); assert(!("creep" in hatori), "Hatori cannot creep"); Object.setPrototypeOf(hatori, kuma); assert("creep" in hatori, "Hatori can now creep"); assert("creep" in yoshi, "Yoshi can also creep");
-
在JavaScript中,对象的原型属性是内置属性(使用标记[prototype]),无法直接访问。相反,内置的方法Object.setPrototypeOf()需要传入两个对象作为参数,并将第二个对象设置为第一个对象的原型。
-
每个对象都可以有一个原型,每个对象的原型也可以拥有一个原型,以此类推,形成一个原型链。查找特定的属性将会被委托在整个原型链上,只有当没有更多的原型可以进行查找时,才会停止查找。
7.2 对象构造器与原型
-
创建一个对象最简单的方法是创建一个空对象。然后通过赋值语句添加属性。
-
通过操作符new,应用于构造函数之前,触发创建一个新对象分匹配。
-
每个函数都有一个原型对象,该原型对象将被自动设置为通过该函数创建对象的原型。
function Ninja(){} Ninja.prototype.swingSword = function(){ return true; }; const ninja1 = Ninja(); assert(ninja1 === undefined, "No instance of Ninja created."); const ninja2 = new Ninja(); assert(ninja2 !== null && ninja2.swingSword && ninja2.swingSword(), "Instance exists and method is callable." );
- 定义了一个名为Ninja的空函数,并通过两种调用方式:
- 一种是普通函数调用
const ninja1 = Ninja();
- 另一种是作为构造器进行调用
const ninja2 = new Ninja();
- 一种是普通函数调用
- 定义了一个名为Ninja的空函数,并通过两种调用方式:
-
当函数创建完成后,立即获得一个原型对象,可以对该原型对象进行扩展。在其原型对象上添加了swingSword方法:
Ninja.prototype.swingSword = function(){ return true; };
-
首先作为普通函数进行调用,并将结果存储在变量ninja1中。由于没有返回值,所以检测ninja1的值为undefined。作为一个简单的函数,Ninja看起来并不是那么有用。
-
然后通过new调用该函数,此次是作为构造器进行调用。再次调用这个函数,但这一次已经创建了新分配的对象,并将其设置为函数的上下文(可通过this访问)。操作符new返回的结果是这个新对象的引用。测试的ninja2是新创建的对象的引用,具有swingSword方法,并调用swingSword方法。
-
创建的每一个函数都具有一个新的原型对象,当我们将一个函数作为构造函数使用时,构造器的原型对象将被设置为函数的原型。
-
每一个函数都有一个原型对象
-
每一个函数的原型对象都有一个constructor属性,该属性指向函数本身。
-
constructor对象的原型设置为新创建的对象的原型
-
-
我们创建的每一个函数都有一个新的原型对象。最初的原型对象只有一个属性,即constructor属性。该属性指向函数本身。
-
当我们将函数作为构造器进行调用时,新构造出来的对象的原型被设置为构造函数的原型的引用。
-
本例中,在Ninja.prototype上增加了swingSword方法,对象ninja2创建完成时,对象ninja2的原型被设置为Ninja的原型。因此,通过ninja2调用方法swingSword,将查找该方法委托到Ninja的原型对象上。所有通过构造器Ninja创建出来的对象都可以访问swingSword方法。
7.2.1 实例属性
-
当把函数作为构造函数,通过new操作符进行调用时,它的上下文被定义为新的对象实例。通过原型暴露属性,通过构造函数的参数进行初始化。
function Ninja(){ this.swung = false; this.swingSword = function(){ return !this.swung; }; } Ninja.prototype.swingSword = function(){ return this.swung; }; const ninja = new Ninja(); assert(ninja.swingSword(), "Called the instance method, not the prototype method.");
- 测试结果为,实例会隐藏原型中与实例方法重名的方法。
- 如果属性中可以查找到属性,将不会查找原型。
-
在构造函数内部,关键字this指向新创建的对象,所以在构造器内添加的属性直接在新的ninja实例上,然后通过ninja访问swingSword属性时,就不需要遍历原型链,立即可以找到并返回在构造器内创建的属性。
-
只在函数的原型上创建对象的方法是很有意义的,这样我们可以使得同一个方法由所有对象实例共享。
- 注意:
- 在构造函数内部定义方法,使得我们可以模仿私有对象变量。
- 如果需要私有对象,在构造函数内指定方法是唯一的解决方案。
- 注意:
7.2.2 JavaScript动态特性的副作用
-
JavaScript是一门动态语言,可以很容易的添加、删除和修改属性。这种特性同样适用于原型,包括函数原型和对象原型。
//通过原型,一切都可以在运行时修改 function Ninja(){ this.swung = true; } const ninja1 = new Ninja(); Ninja.prototype.swingSword = function(){ return this.swung; }; assert(ninja1.swingSword(), "Method exists, even out of order."); Ninja.prototype = { pierce: function() { return true; } } assert(ninja1.swingSword(), "Our ninja can still swing!"); const ninja2 = new Ninja(); assert(ninja2.pierce(),"Newly created ninjas can pierce"); assert(!ninja2.swingSword, "But they cannot swing!");
- 定义了一个Ninja构造器,继续使用它来创建一个实例对象。
- 实例对象创建完之后,我们在原型上添加swingSword方法。通过执行测试来验证可以在对象创建完之后,修改该对象的原型。
- 然后使用字面量对象完全重写Ninja的原型对象,该字面量仅含有pierce方法。
- 即使Ninja函数不再指向旧的Ninja原型,但是旧的原型仍然存在于ninja1实例中,通过原型链仍然能够访问swingSword方法。但是,在Ninja函数发生完全重写Ninja的原型对象后再创建新的实例对象。
- 对象与函数原型之间的引用关系是在对象创建时建立的。新创建的对象将引用新的原型,他只能访问pierce方法,原来旧的对象保持着原有的原型,仍然能访问swingSword方法。
7.2.3 通过构造函数实现对象类型
-
虽然知道JavaScript如何使用原型查找引用属性,这一点很重要,但也需要知道对象实例是通过哪个函数构造创建的。如前文所述,可以通过构造函数的原型中的constructor属性访问对象的构造器。
- 每个函数的原型对象都具有一个constructor属性,可以用来访问创建该对象时所用的函数。这个特性可以用于类型校验。
//检验实例的类型与它的constructor function Ninja(){} const ninja = new Ninja(); assert(typeof ninja === "object", "The type of the instance is object."); assert(ninja instanceof Ninja, "instanceof identifies the constructor." ); assert(ninja.constructor === Ninja, "The ninja object was created by the Ninja function.");
- 首先定义一个构造器,并使用该构造器创建一个对象实例。然后使用操作符typeof检查该实例对象的类型。因为所有的实例都是对象类型,所以返回的类型总是对象。更有趣的是instanceof,它提供了一种用于检测一个实例是否由特定构造函数创建的方法。
- 此外,可以使用constructor属性,所有的实例对象都可以访问constructor属性,constructor属性是创建实例对象的函数的引用。可以使用constructor属性验证实例的原始类型(与操作符instanceof非常类似)。
- 由于constructor属性仅仅是原始构造函数的引用,因此可以使用该属性创建新的Ninja对象。
//使用constructor的引用创建新的对象 function Ninja(){} const ninja = new Ninja(); const ninja2 = new ninja.constructor(); assert(ninja2 instanceof Ninja, "It's a Ninja!"); assert(ninja !== ninja2, "But not the same Ninja!");
- 定义了一个构造器,并使用该构造器创建了一个实例对象。然后使用该实例对象的constructor属性创建第二个实例。验证表明第二个Ninja对象被创建成功,并且第二个实例对象与第一个实例对象是截然不同的两个实例。
- 更有趣的是,不需要访问原始构造函数就可以直接创建对象,即使原始构造函数已经不在作用域内,在这种情景下完全可以使用构造函数的引用。
- 注意:虽然对象的constructor属性有可能发生改变,改变constructor属性没有任何直接或明显的建设性目的。constructor属性的存在仅仅是为了说明该对象是从哪创建出来的。如果重写了constructor属性,那么原始值就被丢失了。
7.3 实现继承
-
继承是一种在新对象上复用现有对象的属性的形式。这有助于避免重复代码和重复数据。在JavaScript中,继承原理与其他流行的面向对象语言略有不同。
//尝试实现原型继承 function Person(){} Person.prototype.dance = function(){}; function Ninja(){} Ninja.prototype = { dance: Person.prototype.dance }; const ninja = new Ninja(); assert(ninja instanceof Ninja,"ninja receives functionality from the Ninja prototype" ); assert(ninja instanceof Person, "... and the Person prototype" ); assert(ninja instanceof Object, "... and the Object prototype" );
- 由于函数原型是对象类型,因此有多种复制功能(如属性或方法)可以实现继承的方法。
- 先定义了Person与Ninja。显然Ninja是一个Person,希望Ninja能够继承Person的属性。试图将Person原型上的dance方法复制到Ninja原型的同名属性上。
- 执行测试后发现,虽然已经教会了Ninja跳舞,但是无法使得Ninja成为真正的Person类型。对Ninja模拟Person的dance方法,但是Ninja仍然不是真实的Person类型。这不是真正的继承——仅仅是复制。
- 由于这种方法是无效的继承,因此我们还需要将每个Person的属性单独复制到Ninja的原型上。
- 真正想要实现的是一个完整的原型链,在原型链上,Ninja继承自Person,Person继承自Mammal,Mammal继承自Animal,以此类推,一直到Object。
- 创建这样的原型链最佳技术方案是一个对象的原型直接是另外一个对象的实力。
SubClass.prototype = new SuperClass()
; - 例如:
Ninja.prototype = new Person();
- 因为SubClasss实例的原型也是SuperClass的实例,SuperClass实例具有SuperClass的全部属性,SuperClass实例也同时具有一个指向超类的原型。
//使用原型实现继承 function Person(){} Person.prototype.dance = function(){}; function Ninja(){} Ninja.prototype = new Person(); const ninja = new Ninja(); assert(ninja instanceof Ninja,"ninja receives functionality from the Ninja prototype" ); assert(ninja instanceof Person, "... and the Person prototype" ); assert(ninja instanceof Object, "... and the Object prototype" );
- 这段代码中唯一的不同是使用Person的实例作为Ninja的原型。运行测试会发现这种方式实现了继承。
- 当定义了一个Person函数时,同时也创建了Person原型,该原型通过其constructor属性引用函数本身。正常来说,可以使用附加属性拓展Person原型,在本例中,我们在Person的原型上拓展了dance方法,因此每个Person的实例对象也都具有dance方法。
- 再次定义一个Ninja函数。该函数的原型也具有一个constructor属性指向函数本身。
- 为了实现继承,将Ninja的原型赋值为Person的实例。现在,每当创建一个新的Ninja对象时,新创建的Ninja对象将设置为Ninja的原型属性所指向的对象,即Person实例。
- 通过ninja对象访问dance方法,JavaScript运行时会首先查找ninja对象本身。由于ninja对象本身不具有dance方法,接下来搜索ninja对象的原型即person对象。person对象也不具有dance方法,所以再接着查找person对象的原型,最终找到了dance方法。这就是JavaScript中实现继承的原理。
- 这里有一个重要的提示:通过执行instanceof操作符,可以判定函数是否继承原型链上的对象功能。
- 这种原型实现继承的方式的副作用好的一面是,所有继承函数的原型将实时更新。从原型继承的对象总是可以访问当前原型属性。
7.3.1 重写constructor属性的问题
- 通过设置Person实例对象作为Ninja构造器的原型时,我们已经丢失了Ninja与Ninja初始原型之间的关联。这是一个问题,因为constructor属性可以用于检测一个对象是否由某一个函数所创建。
- 无法查找到Ninja对象的constructor属性。回到原型上,原型上也没有constructor属性,继续在原型链上追溯,在Person对象的原型上具有指向Person本身的constructor属性。事实上,如果询问Ninja对象的构造函数,得到的答案会是Person,但这个答案是错误的。
配置对象的属性
-
在JavaScript中,对象是通过属性描述进行描述的,可以配置以下关键字。
- configurable——如果设置为true,则可以修改或删除属性。如果设置为false,则不允许修改。
- enumerable——如果设为true,则可在for-in循环对象属性时出现。
- value——执行属性的值,默认undefined。
- writable——如果设置为true,则可以通过赋值语句修改属性值。
- get——定义getter函数,当访问属性时发生调用,不能与value与writable同时使用。
- set——定义setter函数,当对属性赋值时发生调用,也不能与value与writable同时使用。
-
通过简单赋值语句创建对象属性:
ninja.name = 'Yoshi'
-
该赋值语句创建的属性可以被修改或删除、可遍历、可写,Ninja的name属性值被设置为Yoshi,get和set函数均为undefined。
-
如果想调整属性的配置信息,可以使用内置的
Object.defineProperty
方法,传入3个参数:属性所在的对象、属性名和属性描述对象。//配置属性 "use strict"; const ninja = {}; ninja.name = "Yoshi"; ninja.weapon = "kusarigama"; Object.defineProperty(ninja, "sneaky", { configurable: false, enumerable: false, value: true, writable: true }); assert("sneaky" in ninja, "We can access the new property"); for(let prop in ninja){ assert(prop !== undefined, "An enumerated property: " + prop); }
-
首先创建一个空对象,再通过传统赋值语句添加两个属性:name与weapon。接着,使用内置
Object.defineProperty
方法定义属性sneaky,属性描述为不可配置、不可枚举、属性值为true。由于属性sneaky是可写的,所以可以被改变。 -
最后,验证可以访问新创建的sneaky属性,通过for-in循环遍历所有可枚举的属性。
- 虽然可以正常访问sneaky属性,但是在for-in循环中可遍历属性name与weapon,不可遍历新增的sneaky属性。
-
将配置项enumerable设置为false,在for-in循环中无法遍历该属性。
最后解决constructor属性被覆盖的问题
-
为了实现Ninja继承Person,产生了这样的问题:当把Ninja的原型设置为Person的实例对象后,我们丢失了原来在constructor中的Ninja原型。我们不希望丢失constructor属性,constructor属性可用于确定创建对象实例的函数。
-
通过使用
Object.defineProperty
方法在Ninja.ptototype对象上增加新的constructor属性"use strict"; function Person(){} Person.prototype.dance = function(){}; function Ninja(){} Ninja.prototype = new Person(); Object.defineProperty(Ninja.prototype, "constructor", { enumerable: false, value: Ninja, writable: true }); const ninja = new Ninja(); assert(ninja.constructor === Ninja, "Connection from ninja instances to Ninja constructor reestablished!"); for(let prop in Ninja.prototype){ assert(prop === "dance", "The only enumerable property is dance!"); }
-
重新建立了ninja实例与Ninja构造器之间的联系,所以可以确定ninja实例是通过Ninja构造器创建的。如果遍历Ninja.prototype对象,可以确保不会访问到constructor属性。
7.3.2 instanceof操作符
-
在JavaScript中,操作符instanceof使用在原型链中。
ninja instanceof Ninja
-
操作符instanceof用于检测Ninja函数是否存在于ninja实例的原型链中。
function Person(){} function Ninja(){} Ninja.prototype = new Person(); const ninja = new Ninja(); assert(ninja instanceof Ninja, "Our ninja is a Ninja!"); assert(ninja instanceof Person, "A ninja is also a Person. ");
ninja是Ninja的实例,同时也是Person的实例,这符合预期。
-
ninja实例的原型链是由new Person()对象与Person的原型组成的,通过原型链实现继承。当执行
ninja instanceof Ninja
表达式时,JavaScript引擎检查Ninja函数的原型——new Person()对象,是否存在于ninja实例的原型链上。new Person()对象是ninja实例的原型,因此表达式结果为true。 -
在检查
ninja instanceof Person
时,JavaScript引擎查找Person函数的原型,检查它是否存在于ninja实例的原型链上。由于Person的原型的确存在于ninja实例的原型链上,所以Person是new Person()对象的原型,所以Person也是ninja实例的原型。 -
尽管instanceof操作符最常见的用途就是提供一个清晰的方法来确定一个实例是否由一个特定的构造函数创建的,但并不完全是这样。事实上,它会检查操作符右边的函数的原型是否存在于操作符左边的对象的原型链上。
instanceof操作符的警告
-
改变一个构造函数的原型
function Ninja(){} const ninja = new Ninja(); assert(ninja instanceof Ninja, "Our ninja is a Ninja!"); Ninja.prototype = {}; assert(!(ninja instanceof Ninja), "The ninja is now not a Ninja!?");
-
创建一个ninja实例,第一个测试正常。但是,在ninja实例创建完成后,修改Ninja构造函数的原型,再次执行
ninja instanceof Ninja
,我们会发现结果发生了变化。小心函数的原型可以随时发生改变。 -
instanceof操作符真正的语义——检查右边的函数原型是否存在于左边对象的原型链上。
-
7.4 在ES6中使用JavaScript的class
- JavaScript可以让我们使用原型实现继承。
- 虽然JavaScript本身不支持经典的继承,但还是不可避免的进入类的范畴。为了解决类的问题,出现了一些模拟类的继承的JavaScript库。由于每个库的实现方式不同,ECMAScript委员会对“模拟”基于类的继承语法进行标准化。注意是“模拟”。虽然我们现在可以使用class,但其底层的实现仍然是基于原型继承。
7.4.1 使用关键字class
-
ES6引入新的关键字class,它提供了一种更优雅的创建对象和实现继承的方式,底层仍然是基于原型的实现。
//在ES6中创建类 "use strict" class Ninja{ constructor(name){ this.name = name; } swingSword(){ return true; } } const ninja = new Ninja("Yoshi"); assert(ninja instanceof Ninja, "Our ninja is a Ninja"); assert(ninja.name === "Yoshi", "named Yoshi"); assert(ninja.swingSword(), "and he can swing a sword");
-
可以通过使用ES6的关键字class创建Ninja类,在类中创建构造函数,使用类创建实例对象时,调用该构造函数。在构造函数体内,可以通过this访问新创建的实例,添加属性很简单,例如添加name属性。在类中,还可以定义所有实例对象均可访问的方法。本例中定义了一个返回值为true的swingSword方法。
-
通过关键字new创建Ninja类的实例。
class是语法糖
-
虽然ES6引入关键字class,但是底层仍然是基于原型的实现。class只是语法糖,使得JavaScript模拟类的代码更为简洁。
//用ES5实现上面代码 function Ninja(name) { this.name = name; } Ninja.prototype.swingSword = function () { return true; }
-
可以看出,ES6的类没有任何特殊之处。虽然看起来优雅,但使用的是相同的概念。
静态方法
-
类级别的静态方法。
-
"use strict" class Ninja{ constructor(name, level){ this.name = name; this.level = level; } swingSword() { return true; } static compare(ninja1, ninja2){ return ninja1.level - ninja2.level; } } const ninja1 = new Ninja("Yoshi", 4); const ninja2 = new Ninja("Hatori", 3); assert(typeof ninja1.compare === "undefined" && typeof ninja2.compare === "undefined", "The ninja instance doesn’t know how to compare"); assert(Ninja.compare(ninja1, ninja2) > 0, "The Ninja class can do the comparison!"); assert(typeof Ninja.swingSword === "undefined", "The Ninja class can not swing a sword");
-
创建Ninja类,该类具有所有实例对象均可访问的swingSword方法。同时,通过关键字static定义了一个静态方法compare。用于比较两个忍者技能等级的比较方法定义在了类中,而非实例中。接着验证了实例不可访问compare方法,而Ninja类可以访问compare方法。
-
在ES6之前的版本是通过函数来实现类的。由于静态方法是类级别的方法,所以可以利用第一类型对象,在构造函数上添加方法。
function Ninja() {} Ninja.compare = function (ninja1, ninja2) { ...... }
7.4.2 实现继承
-
对所有实例均可访问的方法必须直接添加在构造函数的原型上。为了实现继承,我们必须将实例对象衍生的原型设置成"基类"。糟糕的是,这会弄乱constructor属性,所以需要通过Object.defineProperty方法进行手动设置。为了实现一个相对简单和通用的继承特性,我们需要记住这一系列细节。
-
在ES6中实现继承
"use strict" class Person { constructor(name){ this.name = name; } dance(){ return true; } } class Ninja extends Person { constructor(name, weapon){ super(name); //Uncomment this line of code when super gets supported. this.weapon = weapon; } wieldWeapon(){ return true; } } const person = new Person("Bob"); assert(person instanceof Person, "A person’s a person"); assert(person.dance(), "A person can dance."); assert(person.name === "Bob", "We can call it by name."); assert(!(person instanceof Ninja), "But it’s not a Ninja"); assert(typeof person.wieldWeapon === "undefined", "And it cannot wield a weapon"); const ninja = new Ninja("Yoshi", "Wakizashi"); assert(ninja instanceof Ninja, "A ninja’s a ninja"); assert(ninja.wieldWeapon(), "That can wield a weapon"); assert(ninja instanceof Person, "But it’s also a person"); assert(ninja.name === "Yoshi" , "That has a name"); assert(ninja.dance(), "And enjoys dancing");
-
使用extends从另一个类实现继承
class Ninja extends Person
-
本例中创建Person类,其构造函数对每一个实例对象添加name属性。同时,定义一个所有Person的实例均可访问的dance方法。
class Person { constructor(name){ this.name = name; } dance(){ return true; } }
-
定义一个从Person类继承而来的Ninja类。在Ninja类上添加weapon属性和wieldWeapon方法。
-
衍生类Ninja构造函数通过关键字super调用基类Person的构造函数。
class Ninja extends Person { constructor(name, weapon){ super(name); //Uncomment this line of code when super gets supported. this.weapon = weapon; } wieldWeapon(){ return true; } }
-
继续创建person实例,并验证Person类的实例具有name属性与dance方法,但不具有wieldWeapon方法。
const person = new Person("Bob"); assert(person instanceof Person, "A person’s a person"); assert(person.dance(), "A person can dance."); assert(person.name === "Bob", "We can call it by name."); assert(!(person instanceof Ninja), "But it’s not a Ninja"); assert(typeof person.wieldWeapon === "undefined", "And it cannot wield a weapon");
-
创建一个ninja实例,验证ninja是类Ninja的实例,具有wieldWeapon方法。由于所有的ninja同时也是类Person的实例,因此,ninja实例也具有name属性和dance方法。
const ninja = new Ninja("Yoshi", "Wakizashi"); assert(ninja instanceof Ninja, "A ninja’s a ninja"); assert(ninja.wieldWeapon(), "That can wield a weapon"); assert(ninja instanceof Person, "But it’s also a person"); assert(ninja.name === "Yoshi" , "That has a name"); assert(ninja.dance(), "And enjoys dancing");
-
定义类,并通过关键字extends定义类之间的关系。这样,在ES6中,可以像传统面向对象语言那样简单地实现类。