JavaScript之原型

来源:互联网 发布:阿里云香港主机建立ss 编辑:程序博客网 时间:2024/06/05 14:11

JavaScript之原型

1. 混合对象类


(1) Javascript中的类


相当长的一段时间里,JavaScript只有一些近似类的语法元素(比如 new 和 instanceof ),不过在后来的ES6中新增了一些元素,比如 class 关键字。这是不是意味着JavaScript中实际上有类呢?简单来说:不是


(2) 类的继承


a. 多态

先看一个伪代码的例子
class Vehicle {engines = 1,ignition() {output( "Turning on my engine." );},drive() {ignition();output( "Steering and moving forward!" )}}class Car inherits Vehicle {wheels = 4,drive() {inherited: drive()output( "Rolling on all ", wheels, " wheels!" )}}class SpeedBoat inherits Vehicle {engines = 2,ignition() {output( "Turning on my ", engines, " engines." )},pilot() {inherited:drive()output( "Speeding through the water with ease!" )}}
我们通过定义 Vehicle类来假设一种发动机,一种点火方式,一种驾驶方法。但是你不可能制造一个通用的“交通工具”,因为这个类只是一个抽象的概念。接下来我们定义了两类具体的交通工具: Car 和 SpeedBoat 。它们都从 Vehicle继承了通用的特性并根据自身类别修改了某些特性。汽车需要四个轮子,快艇需要两个发动机,因此它必须启动两个发动机的点火装置。

Car 重写了继承自父类的 drive() 方法,但是之后 Car 调用了 inherited:drive() 方法,这表明 Car 可以引用继承来的原始 drive() 方法。快艇的 pilot() 方法同样引用了原始 drive() 方法。这个技术被称为多态或者虚拟多态

b. 多重继承

JavaScript本身并不提供“多重继承”功能。

(3) 混入


在继承或者实例化时,JavaScript的对象机制并不会自动执行复制行为。简单来说,JavaScript中只有对象,并不存在可以被实例化的“类”。一个对象并不会被复制到其他对象,它们会被关联起来。
由于在其他语言中类表现出来的都是复制行为,因此JavaScript开发者也想出了一个方法来模拟类的复制行为,这个方法就是混入。接下来我们会看到两种类型的混入:显式和隐式。

a. 显示混入

先看个例子
function mixin( sourceObj, targetObj ) {for (var key in sourceObj) {// 只会在不存在的情况下复制if (!(key in targetObj)) {targetObj[key] = sourceObj[key];}}return targetObj;}var Vehicle = {engines: 1,ignition: function() {console.log( "Turning on my engine." );},drive: function() {this.ignition();console.log( "Steering and moving forward!" );}};var Car = mixin( Vehicle, {wheels: 4,drive: function() {Vehicle.drive.call( this );console.log("Rolling on all " + this.wheels + " wheels!");}} );
note: 有一点需要注意,我们处理的已经不再是类了,因为在JavaScript中不存在类, Vehicle和 Car 都是对象,供我们分别进行复制和粘贴。
现在 Car 中就有了一份 Vehicle 属性和函数的副本了。从技术角度来说,函数实际上没有被复制,复制的是函数引用。所以, Car 中的属性 ignition 只是从 Vehicle 中复制过来的对于 ignition() 函数的引用。相反,属性 engines 就是直接从 Vehicle 中复制了值1。Car 已经有了 drive 属性(函数),所以这个属性引用并没有被 mixin 重写,从而保留了 Car 中定义的同名属性,实现了“子类”对“父类”属性的重写

1). 再说多态
 Vehicle.drive.call( this ) 。这就是显式多态。
JavaScript并没有相对多态的机制。所以,由于 Car 和 Vehicle 中都有 drive() 函数,为了指明调用对象,我们必须使用绝对(而不是相对)引用。我们通过名称显式指定 Vehicle 对象并调用它的 drive() 函数。但是如果直接执行 Vehicle.drive() ,函数调用中的 this 会被绑定到 Vehicle 对象而不是 Car 对象,这并不是我们想要的,

我们将上面的Car.drive修改一下,用来测试二者之间的不同:
var Car = mixin( Vehicle, {wheels: 4,engines: 2,drive: function() {console.info('显示多态开始');Vehicle.drive.call( this );console.info('相对多态开始');Vehicle.drive();console.log("Rolling on all " + this.wheels + " wheels!");}} );
请看下图示例:

从上图中绿色编著的部分可以看到,显示多态执行的结果enginess属于Car对象的,而Vehicle.drive()中的enginess则是属于Vehicle的。


b. 寄生继承

显示混入模式的一种变体被称为“寄生继承”,它既是显式的又是隐式的。
// “传统的JS类”Vehiclefunction Vehicle() {this.engines = 1;}Vehicle.prototype.ignition = function() {console.log( "Turning on my engine." );};Vehicle.prototype.drive = function() {this.ignition();console.log( "Steering and moving forward!" );};// “寄生类” Carfunction Car() {// 首先,car是一个Vehiclevar car = new Vehicle();// 接着我们对car进行定制car.wheels = 4;// 保存到Vehicle::drive()的特殊引用var vehDrive = car.drive;// 重写Vehicle::drive()car.drive = function() {vehDrive.call( this );console.log("Rolling on all " + this.wheels + " wheels!");}return car;}var myCar = new Car();myCar.drive();
首先复制一份 Vehicle父类(对象)的定义,然后混入子类(对象)的定义(如果需要的话保留到父类的特殊引用),然后用这个复合对象构建实例.

c. 隐式混入

var Something = {cool: function() {this.greeting = "Hello World";this.count = this.count ? this.count + 1 : 1;}};Something.cool();Something.greeting; // "Hello World"Something.count; // 1var Another = {cool: function() {// 隐式把Something混入AnotherSomething.cool.call( this );}};Another.cool();Another.greeting; // "Hello World"Another.count; // 1 (count不是共享状态)
方法调用中使用 Something.cool.call( this ) ,我们实际上“借用”了函数 Something.cool() 并在 Another 的上下文中调用了它。最终的结果是 Something.cool() 中的赋值操作都会应用在 Another 对象上而不是 Something 对象上。因此,我们把 Something 的行为“混入”到了 Another 中

(4) summary


类意味着复制。传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中。

多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父类,但是本质上引用的其实是复制的结果。

JavaScript并不会(像类那样)自动创建对象的副本。

混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆弱的语法,比如显式伪多态( OtherObj.methodName.call(this, ...) ),这会让代码更加难懂并且难以维护。

此外,显式混入实际上无法完全模拟类的复制行为,因为对象(和函数!别忘了函数也是对象)只能复制引用,无法复制被引用的对象或者函数本身。忽视这一点会导致许多问题。

总地来说,在JavaScript中模拟类是得不偿失的,虽然能解决当前的问题,但是可能会埋下更多的隐患.

2. 原型


(1) [[Prototype]]


a. prototype

JavaScript中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对于其他对象的引用。几乎所
有的对象在创建时 [[Prototype]] 属性都会被赋予一个非空的值
var anotherObject = {a:2};// 创建一个关联到anotherObject的对象var myObject = Object.create( anotherObject );myObject.a; // 2
现在 myObject 对象的 [[Prototype]] 关联到了 anotherObject 。显然 myObject.a 并不存在,但是尽管如此,属性访问仍然成功地(在 anotherObject 中)找到了值2。但是,如果 anotherObject 中也找不到 a 并且 [[Prototype]] 链不为空的话,就会继续查找下去。这个过程会持续到找到匹配的属性名或者查找完整条 [[Prototype]] 链。
如果差找不到的话返回值为undefined。
Note: for in 可以遍历对象可枚举的自有属性和可枚举的继承属性。

b. Object.prototype

所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype 。由于所有的“普通”(内置,不是
特定主机的扩展)对象都“源于”(或者说把 [[Prototype]] 链的顶端设置为)这个 Object.prototype 对
象,所以它包含JavaScript中许多通用的功能

c. 属性设置和屏蔽

 myObject.foo = "bar";
1). myObject存在foo属性,并且这个属性为普通属性,则执行赋值操作。 
2). 如果在 [[Prototype]] 链上层存在名为 foo 的普通数据访问属性并且没有被标记为
只读( writable:false ),那就会直接在 myObject 中添加一个名为 foo 的新属性,它是屏蔽属性。
3). 如果在 [[Prototype]] 链上层存在 foo ,但是它被标记为只读( writable:false ),那么无法修改已
有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条
赋值语句会被忽略。总之,不会发生屏蔽。
4). 如果在 [[Prototype]] 链上层存在 foo 并且它是一个setter(参见第3章),那就一定会调用这个
setter。 foo 不会被添加到(或者说屏蔽于) myObject ,也不会重新定义 foo 这个setter。如果也存在get的话,就会在myObject上添加对应的属性,产生屏蔽。
/*第2)种情况*/var parentObject = {foo: 2}var myObject = Object.create(parentObject);myObject.foo = 3;console.log(myObject.foo);console.log(parentObject.foo);/*第3)种情况*/var parentObject = Object.defineProperty({}, "foo" , {value: 2,writable: false})var myObject = Object.create(parentObject);myObject.foo = 3;console.log(myObject.foo);console.log(parentObject.foo);/*第4)种情况*/var parentObject = Object.defineProperty({}, "foo" , {set: function(value) {this._foo = value;}})parentObject._foo = 2;var myObject = Object.create(parentObject);myObject.foo = 3;console.log(myObject.foo);console.log(parentObject.foo);

d. 隐式产生屏蔽

var anotherObject = {a:2};var myObject = Object.create( anotherObject );anotherObject.a; // 2myObject.a; // 2anotherObject.hasOwnProperty( "a" ); // truemyObject.hasOwnProperty( "a" ); // falsemyObject.a++; // 隐式屏蔽!anotherObject.a; // 2myObject.a; // 3myObject.hasOwnProperty( "a" ); // true
myObject.a++; // 隐式屏蔽!执行这行代码的时候执行的是 myObject.a = myObject.a + 1,其实这个地方就要回到上面的说的赋值的第1)种情况了。

(2) 类


JavaScript和面向类的语言不同,它并没有类来作为对象的抽象模式或者说蓝图。JavaScript中只有对象。
实际上,JavaScript才是真正应该被称为“面向对象”的语言,因为它是少有的可以不通过类,直接创建对象的语言。
在JavaScript中,类无法描述对象的行为,(因为根本就不存在类!)对象直接定义自己的行为。再说
一遍,JavaScript中只有对象。

a. 类函数

JavaScript中有一种奇怪的行为一直在被使用,那就是模仿类,这种奇怪的“类似类”的行为利用了函数的一种特殊特性:所有的函数默认都会拥有一个名为 prototype 的公有并且不可枚举的属性,它会指向另一个对象
function Foo() {// ...}Foo.prototype; // { }
Note: 这个对象通常被称为 Foo 的原型,因为我们通过名为 Foo.prototype 的属性引用来访问它。然而不幸
的是,这个术语对我们造成了极大的误导,稍后我们就会看到. 我们现在县称之为“被贴上‘Foo点prototype’标签的对象”

对应这个对象的最直接的解释就是: 这个对象是在调用 new Foo()时创建的,最后会被(有点武断地)关联到这个“Foo点prototype”对象上

验证一下:
function Foo() {// ...}var a = new Foo();Object.getPrototypeOf( a ) === Foo.prototype; // true
调用 new Foo() 时会创建 a (具体的4个步骤参见前一篇文章this),其中的一步就是给 a 一个内部
的 [[Prototype]] 链接,关联到 Foo.prototype 指向的那个对象

Note: 思考这条语句的含义
在面向类的语言中,类可以被复制(或者说实例化)多次,就像用模具制作东西一样。我们在第4章中看到过,之所以会这样是因为实例化(或者继承)一个类就意味着“把类的行为复制到物理对象中”,对于每一个新实例来说都会重复这个过程。

但是在JavaScript中,并没有类似的复制机制。你不能创建一个类的多个实例,只能创建多个对象,它们 [[Prototype]] 关联的是同一个对象。但是在默认情况下并不会进行复制,因此这些对象之间并不会完全失去联系,它们是互相关联的。new Foo() 会生成一个新对象(我们称之为 a ),这个新对象的内部链接 [[Prototype]] 关联的是 Foo.prototype 对象。

最后我们得到了两个对象,它们之间互相关联,就是这样。我们并没有初始化一个类,实际上我们并没有从“类”中复制任何行为到一个对象中,只是让两个对象互相关联。

实际上,绝大多数JavaScript开发者不知道的秘密是, new Foo() 这个函数调用实际上并没有直接创建关联,这个关联只是一个意外的副作用。 new Foo() 只是间接完成了我们的目标:一个关联到其他对象的新对象。

Note: 另外的方式是通过 Object.create(..)

b. 构造函数

function Foo() {// ...}var a = new Foo();
到底是什么让我们认为 Foo 是一个“类”呢?
其中一个原因是我们看到了关键字 new ,在面向类的语言中构造类实例时也会用到它。另一个原因是,看起来我们执行了类的构造函数方法, Foo() 的调用方式很像初始化类时类构造函数的调用方式

除了令人迷惑的“构造函数”语义外, Foo.prototype 还有另一个绝招
function Foo() {// ...}Foo.prototype.constructor === Foo; // truevar a = new Foo();a.constructor === Foo; // true
Foo.prototype 默认(在代码中第一行声明时!)有一个公有并且不可枚举的属性 .constructor ,这个属性引用的是对象关联的函数(本例中是 Foo )。请看下图


此外,我们可以看到通过“构造函数”调用 new Foo() 创建的对象也有一个 .constructor 属性,指向“创建这个对象的函数”,请看下图


 a 本身并没有 .constructor 属性。而且,虽然 a.constructor 确实指向 Foo 函数,从上图中可以看到其实是原型上面的属性,其实这也是上一副图中的一种mapping吧。但是这
个属性并不是表示 a 由 Foo “构造”。

构造函数还是调用
上一段代码很容易让人认为 Foo 是一个构造函数,因为我们使用 new 来调用它并且看到它“构造”了一个对象。实际上, Foo 和你程序中的其他函数没有任何区别。函数本身并不是构造函数,然而,当你在普通的函数调用前面加上 new 关键字之后,就会把这个函数调用变成一个“构造函数调用”。实际上, new 会劫持所有普通函数并用构造对象的形式来调用它。

使用 new 调用时,它就会构造一个对象并赋值给 a ,这看起来像是 new 的一个副作用(无论如何都会构造一个对象)。这个调用是一个构造函数调用,但是函数 本身并不是一个构造函数。换句话说,在JavaScript中对于“构造函数”最准确的解释是,所有带 new 的函数调用,希望大家在回顾一下new执行的几个步骤,函数不是构造函数,但是当且仅当使用 new 时,函数调用会变成“构造函数调用”

c. 技术

function Foo(name) {this.name = name;}Foo.prototype.myName = function() {return this.name;};var a = new Foo( "a" );var b = new Foo( "b" );a.myName(); // "a"b.myName(); // "b"
1). this.name = name 给每个对象(也就是 a 和 b ,参见第2章中的 this 绑定)都添加了 .name 属性,有点
像类实例封装的数据值。
2). Foo.prototype.myName = ... 可能个更有趣的技巧,它会给 Foo.prototype 对象添加一个属性(函
数)。现在, a.myName() 可以正常工作,在这段代码中,看起来似乎创建 a 和 b 时会把 Foo.prototype 对象复制到这两个对象中,然而事实并不是这样,前面介绍默认我们介绍过 [[Prototype]] 链,以及当属性不直接存在于对象中时如何通过它来进行查找。当我们在对象上无法找到属性是,会访问原型链。

(3)原型继承


a. 典型的“原型风格”

function Foo(name) {this.name = name;}Foo.prototype.myName = function() {return this.name;};function Bar(name,label) {Foo.call( this, name );this.label = label;}// 我们创建了一个新的Bar.prototype对象并关联到Foo.prototypeBar.prototype = Object.create( Foo.prototype );// 注意!现在没有Bar.prototype.constructor了// 如果你需要这个属性的话可能需要手动修复一下它Bar.prototype.myLabel = function() {return this.label;};var a = new Bar( "a", "obj a" );a.myName(); // "a"a.myLabel(); // "obj a"

b. 常见的两种错误方式

1). Bar.prototype = Foo.prototype;
Bar.prototype = Foo.prototype 并不会创建一个关联到 Bar.prototype 的新对象,它只是让 Bar.prototype 直接引用 Foo.prototype 对象。因此当你执行类似 Bar.prototype.myLabel = ... 的赋值语句时会直接修改 Foo.prototype 对象本身,会在Foo.prototype添加新的函数。

2). Bar.prototype = new Foo();
会创建一个关联到 Bar.prototype 的新对象。但是它使用了 Foo(..)的“构造函数调用”,如果函数 Foo 有一些副作用(比如写日志、修改状态、注册到其他对象、给 this 添加数据属性,等等)的话,就会影响到 Bar() 的“后代”。

Summary: 因此,要创建一个合适的关联对象,我们必须使用 Object.create(..) 而不是使用具有副作用的 Foo(..) 。这样做唯一的缺点就是需要创建一个新对象然后把旧对象抛弃掉,不能直接修改已有的默认对象。


c.  Object.setPrototypeOf(..) 

ES6添加了辅助函数 Object.setPrototypeOf(..) ,可以用标准并且可靠的方法来修改关联。
// ES6之前需要抛弃默认的Bar.prototypeBar.ptototype = Object.create( Foo.prototype );
// ES6开始可以直接修改现有的Bar.prototypeObject.setPrototypeOf( Bar.prototype, Foo.prototype );

(4) 对象关联

[[Prototype]] 机制就是存在于对象中的一个内部链接,它会引用其他对象这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在 [[Prototype]] 关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的 [[Prototype]] ,以此类推。这一系列对象的链接被称为“原型链”。

Object.create(..) 会创建一个新对象( bar )并把它关联到我们指定的对象( foo ),这样我们就可以充分发挥 [[Prototype]] 机制的威力(委托)并且避免不必要的麻烦(比如使用 new 的构造函数调用会生成 .prototype 和 .constructor 引用)。

(5) Object.create(null) 

Object.create(null) 会创建一个拥有空(或者说 null ) [[Prototype]] 链接的对象。由于这个对象没有原型链,所以 instanceof 操作符无法进行判断,因此总是会返回 false 。这些特殊的空 [[Prototype]] 对象通常被称作“字典”,它们完全不会受到原型链的干扰,因此非常适合用来存储数据。
请看图示:


0 0
原创粉丝点击