www.bifa88.com 6

JavaScript的原型承接详解,承接的兑现方式及原型概述

继承的实现方式及原型概述

2015/07/15 · JavaScript
· 原型,
继承

原文出处: 名一的博客   

对于 OO 语言,有一句话叫“Everything is object”,虽然 JavaScript
不是严格意义上的面向对象语言,但如果想要理解 JS
中的继承,这句话必须时刻铭记于心。

JS
的语法非常灵活,所以有人觉得它简单,因为怎么写都是对的;也有人觉得它难,因为很难解释某些语法的设计,谁能告诉我为什么
typeof null 是 object 而 typeof undefined 是 undefined 吗?并且这是在
null == undefined
的前提下。很多我们自认为“懂”了的知识点,细细琢磨起来,还是会发现有很多盲点,“无畏源于无知”吧……

JavaScript的原型继承详解

   JavaScript是一门面向对象的语言。在JavaScript中有一句很经典的话,万物皆对象。既然是面向对象的,那就有面向对象的三大特征:封装、继承、多态。这里讲的是JavaScript的继承,其他两个容后再讲。

  JavaScript的继承和C++的继承不大一样,C++的继承是基于类的,而JavaScript的继承是基于原型的。

  现在问题来了。

  原型是什么?原型我们可以参照C++里的类,同样的保存了对象的属性和方法。例如我们写一个简单的对象

  代码如下:

  function Animal(name) {

  this.name = name;

  }

  Animal.prototype.setName = function(name) {

  this.name = name;

  }

  var animal = new Animal(“wangwang”);

  我们可以看到,这就是一个对象Animal,该对象有个属性name,有个方法setName。要注意,一旦修改prototype,比如增加某个方法,则该对象所有实例将同享这个方法。例如

  代码如下:

  function Animal(name) {

  this.name = name;

  }

  var animal = new Animal(“wangwang”);

  这时animal只有name属性。如果我们加上一句,

  代码如下:

  Animal.prototype.setName = function(name) {

  this.name = name;

  }

  这时animal也会有setName方法。

  继承本复制——从空的对象开始我们知道,JS的基本类型中,有一种叫做object,而它的最基本实例就是空的对象,即直接调用new
Object()生成的实例,或者是用字面量{
}来声明。空的对象是“干净的对象”,只有预定义的属性和方法,而其他所有对象都是继承自空对象,因此所有的对象都拥有这些预定义的
属性与方法。原型其实也是一个对象实例。原型的含义是指:如果构造器有一个原型对象A,则由该构造器创建的实例都必然复制自A。由于实例复制自对象A,所以实例必然继承了A的所有属性、方法和其他性质。那么,复制又是怎么实现的呢?方法一:构造复制每构造一个实例,都从原型中复制出一个实例来,新的实例与原型占用了相同的内存空间。这虽然使得obj1、obj2与它们的原型“完全一致”,但也非常不经济——内存空间的消耗会急速增加。如图:

www.bifa88.com 1

  方法二:写时复制这种策略来自于一致欺骗系统的技术:写时复制。这种欺骗的典型示例就是操作系统中的动态链接库(DDL),它的内存区总是写时复制的。如图:

www.bifa88.com 2

  我们只要在系统中指明obj1和obj2等同于它们的原型,这样在读取的时候,只需要顺着指示去读原型即可。当需要写对象(例如obj2)的属性时,我们就复制一个原型的映像出来,并使以后的操作指向该映像即可。如图:

www.bifa88.com 3

  这种方式的优点是我们在创建实例和读属性的时候不需要大量内存开销,只在第一次写的时候会用一些代码来分配内存,并带来一些代码和内存上的开销。但此后就不再有这种开销了,因为访问映像和访问原型的效率是一致的。不过,对于经常进行写操作的系统来说,这种方法并不比上一种方法经济。方法三:读遍历这种方法把复制的粒度从原型变成了成员。这种方法的特点是:仅当写某个实例的成员,将成员的信息复制到实例映像中。当写对象属性时,例如(obj2.value=10)时,会产生一个名为value的属性值,放在obj2对象的成员列表中。看图:

www.bifa88.com 4

  可以发现,obj2仍然是一个指向原型的引用,在操作过程中也没有与原型相同大小的对象实例创建出来。这样,写操作并不导致大量的内存分配,因此内存的使用上就显得经济了。不同的是,obj2(以及所有的对象实例)需要维护一张成员列表。这个成员列表遵循两条规则:保证在读取时首先被访问到如果在对象中没有指定属性,则尝试遍历对象的整个原型链,直到原型为空或或找到该属性。原型链后面会讲。显然,三种方法中,读遍历是性能最优的。所以,JavaScript的原型继承是读遍历的。constructor熟悉C++的人看完最上面的对象的代码,肯定会疑惑。没有class关键字还好理解,毕竟有function关键字,关键字不一样而已。但是,构造函数呢?实际上,JavaScript也是有类似的构造函数的,只不过叫做构造器。在使用new运算符的时候,其实已经调用了构造器,并将this绑定为对象。例如,我们用以下的代码

  代码如下:

  var animal = Animal(“wangwang”);

  animal将是undefined。有人会说,没有返回值当然是undefined。那如果将Animal的对象定义改一下:

  代码如下:

  function Animal(name) {

  this.name = name;

  return this;

  }

  猜猜现在animal是什么?

  此时的animal变成window了,不同之处在于扩展了window,使得window有了name属性。这是因为this在没有指定的情况下,默认指向window,也即最顶层变量。只有调用new关键字,才能正确调用构造器。那么,如何避免用的人漏掉new关键字呢?我们可以做点小修改:

  代码如下:

  function Animal(name) {

  if(!(this instanceof Animal)) {

  return new Animal(name);

  }

  this.name = name;

  }

  这样就万无一失了。构造器还有一个用处,标明实例是属于哪个对象的。我们可以用instanceof来判断,但instanceof在继承的时候对祖先对象跟真正对象都会返回true,所以不太适合。constructor在new调用时,默认指向当前对象。

  代码如下:

  console.log(Animal.prototype.constructor === Animal); // true

  我们可以换种思维:prototype在函数初始时根本是无值的,实现上可能是下面的逻辑

  // 设定__proto__是函数内置的成员,get_prototyoe()是它的方法

  代码如下:

  var __proto__ = null;

  function get_prototype() {

  if(!__proto__) {

  __proto__ = new Object();

  __proto__.constructor = this;

  }

  return __proto__;

  }

  这样的好处是避免了每声明一个函数都创建一个对象实例,节省了开销。constructor是可以修改的,后面会讲到。基于原型的继承继承是什么相信大家都差不多知道,就不秀智商下限了。

  JS的继承有好几种,这里讲两种

www.bifa88.com,  1. 方法一这种方法最常用,安全性也比较好。我们先定义两个对象

  代码如下:

  function Animal(name) {

  this.name = name;

  }

  function Dog(age) {

  this.age = age;

  }

  var dog = new Dog(2);

  要构造继承很简单,将子对象的原型指向父对象的实例(注意是实例,不是对象)

  代码如下:

  Dog.prototype = new Animal(“wangwang”);

  这时,dog就将有两个属性,name和age。而如果对dog使用instanceof操作符

  代码如下:

  console.log(dog instanceof Animal); // true

  console.log(dog instanceof Dog); // false

  这样就实现了继承,但是有个小问题

  代码如下:

  console.log(Dog.prototype.constructor === Animal); // true

  console.log(Dog.prototype.constructor === Dog); // false

  可以看到构造器指向的对象更改了,这样就不符合我们的目的了,我们无法判断我们new出来的实例属于谁。因此,我们可以加一句话:

  代码如下:

  Dog.prototype.constructor = Dog;

  再来看一下:

  复制代码 代码如下:

  console.log(dog instanceof Animal); // false

  console.log(dog instanceof Dog); // true

  done。这种方法是属于原型链的维护中的一环,下文将详细阐述。2.
方法二这种方法有它的好处,也有它的弊端,但弊大于利。先看代码

  代码如下:

  function Animal(name) {

  this.name = name;

  }

  Animal.prototype.setName = function(name) {

  this.name = name;

  }

  function Dog(age) {

  this.age = age;

  }

  Dog.prototype = Animal.prototype;

  这样就实现了prototype的拷贝。

  这种方法的好处就是不需要实例化对象(和方法一相比),节省了资源。弊端也是明显,除了和上文一样的问题,即constructor指向了父对象,还只能复制父对象用prototype声明的属性和方法。也即是说,上述代码中,Animal对象的name属性得不到复制,但能复制setName方法。最最致命的是,对子对象的prototype的任何修改,都会影响父对象的prototype,也就是两个对象声明出来的实例都会受到影响。所以,不推荐这种方法。

  原型链

  写过继承的人都知道,继承可以多层继承。而在JS中,这种就构成了原型链。上文也多次提到了原型链,那么,原型链是什么?一个实例,至少应该拥有指向原型的proto属性,这是JavaScript中的对象系统的基础。不过这个属性是不可见的,我们称之为“内部原型链”,以便和构造器的prototype所组成的“构造器原型链”(亦即我们通常所说的“原型链”)区分开。我们先按上述代码构造一个简单的继承关系:

  代码如下:

  function Animal(name) {

  this.name = name;

  }

  function Dog(age) {

  this.age = age;

  }

  var animal = new Animal(“wangwang”);

  Dog.prototype = animal;

  var dog = new Dog(2);

  提醒一下,前文说过,所有对象都是继承空的对象的。所以,我们就构造了一个原型链:

www.bifa88.com 5

  我们可以看到,子对象的prototype指向父对象的实例,构成了构造器原型链。子实例的内部proto对象也是指向父对象的实例,构成了内部原型链。当我们需要寻找某个属性的时候,代码类似于

  代码如下:

  function getAttrFromObj(attr, obj) {

  if(typeof(obj) === “object”) {

  var proto = obj;

  while(proto) {

  if(proto.hasOwnProperty(attr)) {

  return proto[attr];

  }

  proto = proto.__proto__;

  }

  }

  return undefined;

  }

  在这个例子中,我们如果在dog中查找name属性,它将在dog中的成员列表中寻找,当然,会找不到,因为现在dog的成员列表只有age这一项。接着它会顺着原型链,即.proto指向的实例继续寻找,即animal中,找到了name属性,并将之返回。假如寻找的是一个不存在的属性,在animal中寻找不到时,它会继续顺着.proto寻找,找到了空的对象,找不到之后继续顺着.proto寻找,而空的对象的.proto指向null,寻找退出。

  原型链的维护我们在刚才讲原型继承的时候提出了一个问题,使用方法一构造继承时,子对象实例的constructor指向的是父对象。这样的好处是我们可以通过constructor属性来访问原型链,坏处也是显而易见的。一个对象,它产生的实例应该指向它本身,也即是

  代码如下:

  (new obj()).prototype.constructor === obj;

  然后,当我们重写了原型属性之后,子对象产生的实例的constructor不是指向本身!这样就和构造器的初衷背道而驰了。我们在上面提到了一个解决方案:

  代码如下:

  Dog.prototype = new Animal(“wangwang”);

  Dog.prototype.constructor = Dog;

  看起来没有什么问题了。但实际上,这又带来了一个新的问题,因为我们会发现,我们没法回溯原型链了,因为我们没法寻找到父对象,而内部原型链的.proto属性是无法访问的。于是,SpiderMonkey提供了一个改良方案:在任何创建的对象上添加了一个名为__proto__的属性,该属性总是指向构造器所用的原型。这样,对任何constructor的修改,都不会影响__proto__的值,就方便维护constructor了。

  但是,这样又两个问题:

  __proto__是可以重写的,这意味着使用它时仍然有风险

  __proto__是spiderMonkey的特殊处理,在别的引擎(例如JScript)中是无法使用的。

  我们还有一种办法,那就是保持原型的构造器属性,而在子类构造器函数内初始化实例的构造器属性。

  代码如下:改写子对象

  代码如下:

  function Dog(age) {

  this.constructor = arguments.callee;

  this.age = age;

  }

  Dog.prototype = new Animal(“wangwang”);

  这样,所有子对象的实例的constructor都正确的指向该对象,而原型的constructor则指向父对象。虽然这种方法的效率比较低,因为每次构造实例都要重写constructor属性,但毫无疑问这种方法能有效解决之前的矛盾。ES5考虑到了这种情况,彻底的解决了这个问题:可以在任意时候使用Object.getPrototypeOf()
来获得一个对象的真实原型,而无须访问构造器或维护外部的原型链。因此,像上一节所说的寻找对象属性,我们可以如下改写:

  代码如下:

  function getAttrFromObj(attr, obj) {

  if(typeof(obj) === “object”) {

  do {

  var proto = Object.getPrototypeOf(dog);

  if(proto[attr]) {

  return proto[attr];

  }

  }

  while(proto);

  }

  return undefined;

  }

  当然,这种方法只能在支持ES5的浏览器中使用。为了向后兼容,我们还是需要考虑上一种方法的。更合适的方法是将这两种方法整合封装起来,这个相信读者们都非常擅长,这里就不献丑了。

JavaScript是一门面向对象的语言。在JavaScript中有一句很经典的话,万物皆对象。既然是面向对象的,那就有面向对象…

JavaScript是一门面向对象的语言。在JavaScript中有一句很经典的话,万物皆对象。既然是面向对象的,那就有面向对象的三大特征:封装、继承、多态。这里讲的是JavaScript的继承,其他两个容后再讲。

1. 简单对象

既然是讲继承,自然是从最简单的对象说起:

JavaScript

var dog = { name: ‘tom’ }

1
2
3
var dog = {
  name: ‘tom’
}

这便是对象直接量了。每一个对象直接量都是 Object 的子类,即

JavaScript

dog instanceof Object; // true

1
dog instanceof Object; // true

JavaScript的继承和C++的继承不大一样,C++的继承是基于类的,而JavaScript的继承是基于原型的。

2. 构造函数

JS 中的构造函数与普通函数并没有什么两样,只不过在调用时,前面加上了 new
关键字,就当成是构造函数了。

JavaScript

function Dog(name) { this.name = name; } var dog = new Dog(‘tom’); dog
instanceof Dog; // true

1
2
3
4
5
6
7
function Dog(name) {
  this.name = name;
}
 
var dog = new Dog(‘tom’);
 
dog instanceof Dog; // true

两个问题,第一,不加 new 关键字有什么后果?

那么 Dog 函数中的 this
在上下文(Context)中被解释为全局变量,具体在浏览器端的话是 window
对象,在 node 环境下是一个 global 对象。

第二,dog 的值是什么?很简单,undefined 。Dog
函数没有返回任何值,执行结束后,dog 的值自然是 undefined 。

关于 new
的过程,这里也顺便介绍一下,这个对后面理解原型(prototype)有很大的帮助:

  1. 创建一个空的对象,仅包含 Object 的属性和方法。
  2. 将 prototype 中的属性和方法创建一份引用,赋给新对象。
  3. 将 this 上的属性和方法新建一份,赋给新对象。
  4. 返回 this 对象,忽略 return 语句。

需要明确的是,prototype 上的属性和方法是实例间共享的,this
上的属性和方法是每个实例独有的。

现在问题来了。

3. 引入 prototype

现在为 Dog 函数加上 prototype,看一个例子:

JavaScript

function Dog(name) { this.name = name; this.bark = function() {}; }
Dog.prototype.jump = function() {}; Dog.prototype.species = ‘Labrador’;
Dog.prototype.teeth = [‘1’, ‘2’, ‘3’, ‘4’]; var dog1 = new Dog(‘tom’),
dog2 = new Dog(‘jerry’); dog1.bark !== dog2.bark; // true dog1.jump ===
dog2.jump; // true dog1.teeth.push(‘5’); dog2.teeth; // [‘1’, ‘2’, ‘3’,
‘4’, ‘5’]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Dog(name) {
  this.name = name;
  this.bark = function() {};
}
 
Dog.prototype.jump = function() {};
Dog.prototype.species = ‘Labrador’;
Dog.prototype.teeth = [‘1’, ‘2’, ‘3’, ‘4’];
 
var dog1 = new Dog(‘tom’),
    dog2 = new Dog(‘jerry’);
 
dog1.bark !== dog2.bark; // true
dog1.jump === dog2.jump; // true
 
dog1.teeth.push(‘5’);
dog2.teeth; // [‘1’, ‘2’, ‘3’, ‘4’, ‘5’]

看到有注释的那三行应该可以明白“引用”和“新建”的区别了。

那么我们经常说到的“原型链”到底是什么呢?这个术语出现在继承当中,它用于表示对象实例中的属性和方法来自于何处(哪个父类)。好吧,这是笔者的解释。

JavaScript

– Object bark: Dog/this.bark() name: ‘tom’ – __proto__: Object
jump: Dog.prototype.jump() species: ‘Labrador’ + teeth: Array[4] +
constructor: Dog() + __proto__: Object

1
2
3
4
5
6
7
8
9
– Object
  bark: Dog/this.bark()
  name: ‘tom’
– __proto__: Object
    jump: Dog.prototype.jump()
    species: ‘Labrador’
  + teeth: Array[4]
  + constructor: Dog()
  + __proto__: Object  

上面的是 dog1 的原型链,不知道够不够直观地描述“链”这个概念。

  1. 其中,bark 和 name 是定义在 this 中的,所以最顶层可以看到它俩。
  2. 然后,每一个对象都会有一个 __proto__ 属性(IE
    11+),它表示定义在原型上的属性和方法,所以 jump、species 和 teeth
    自然就在这儿了。
  3. 最后就一直向上找 __proto__ 中的属性和方法。

  4. 继承的几种实现


原型是什么?原型我们可以参照C++里的类,同样的保存了对象的属性和方法。例如我们写一个简单的对象

4.1 通过 call 或者 apply

继承在编程中有两种说法,一个叫 inherit,另一个是 extend
。前者是严格意义上的继承,即存在父子关系,而后者仅仅是一个类扩展了另一个类的属性和方法。那么
call 和 apply 就属于后者的范畴。怎么说?

JavaScript

function Animal(gender) { this.gender = gender; } function Dog(name,
gender) { Animal.call(this, gender); this.name = name; } var dog = new
Dog(‘tom’, ‘male’); dog instanceof Animal; // false

1
2
3
4
5
6
7
8
9
10
11
12
function Animal(gender) {
  this.gender = gender;
}
 
function Dog(name, gender) {
  Animal.call(this, gender);
  this.name = name;
}
 
var dog = new Dog(‘tom’, ‘male’);
 
dog instanceof Animal; // false

虽然在 dog 对象中有 gender 属性,但 dog 却不是 Animal
类型。甚至,这种方式只能“继承”父类在 this 上定义的属性和方法,并不能继承
Animal.prototype 中的属性和方法。

复制代码 代码如下:

4.2 通过 prototype 实现继承

要实现继承,必须包含“原型”的概念。下面是很常用的继承方式。

JavaScript

function Dog(name) { Animal.call(this); } Dog.prototype = new Animal();
// 先假设 Animal 函数没有参数 Dog.prototype.constructor = Dog; var dog =
new Dog(‘tom’); dog instanceof Animal; // true

1
2
3
4
5
6
7
8
9
10
function Dog(name) {
  Animal.call(this);
}
 
Dog.prototype = new Animal(); // 先假设 Animal 函数没有参数
Dog.prototype.constructor = Dog;
 
var dog = new Dog(‘tom’);
 
dog instanceof Animal; // true

继承的结果有两个:一、获得父类的属性和方法;二、正确通过 instanceof
的测试。

prototype 也是对象,它是创建实例时的装配机,这个在前面有提过。new
Animal() 的值包含 Animal 实例所有的属性和方法,既然它赋给了 Dog 的
prototype,那么 Dog 的实例自然就获得了父类的所有属性和方法。

并且,通过这个例子可以知道,改变 Dog 的 prototype 属性可以改变
instanceof 的测试结果,也就是改变了父类。

然后,为什么要在 Dog 的构造函数中调用 Animal.call(this)?

因为 Animal 中可能在 this
上定义了方法和函数,如果没有这句话,那么所有的这一切都会给到 Dog 的
prototype 上,根据前面的知识我们知道,prototype
中的属性和方法在实例间是共享的。

我们希望将这些属性和方法依然保留在实例自身的空间,而不是共享,因此需要重写一份。

至于为什么要修改
constructor,只能说是为了正确的显示原型链吧,它并不会影响 instanceof
的判断。或者有其他更深的道理我并不知道……

function Animal(name) {
    this.name = name;
}
Animal.prototype.setName = function(name) {
    this.name = name;
}
var animal = new Animal(“wangwang”);

4.3 利用空对象实现继承

上面的继承方式已经近乎完美了,除了两点:

一、Animal 有构造参数,并且使用了这些参数怎么办?
二、在 Dog.prototype 中多了一份定义在 Animal 实例中冗余的属性和方法。

JavaScript

function Animal(name) { name.doSomething(); } function Dog(name) {
Animal.call(this, name); } Dog.prototype = new Animal(); //
由于没有传入name变量,在调用Animal的构造函数时,会出错
Dog.prototype.constructor = Dog;

1
2
3
4
5
6
7
8
9
10
function Animal(name) {
  name.doSomething();
}
 
function Dog(name) {
  Animal.call(this, name);
}
 
Dog.prototype = new Animal(); // 由于没有传入name变量,在调用Animal的构造函数时,会出错
Dog.prototype.constructor = Dog;

这个问题可以通过一个空对象来解决(改自 Douglas Crockford)。

JavaScript

function DummyAnimal() {} DummyAnimal.prototype = Animal.prototype;
Dog.prototype = new DummyAnimal(); Dog.prototype.constructor = Dog;

1
2
3
4
5
function DummyAnimal() {}
DummyAnimal.prototype = Animal.prototype;
 
Dog.prototype = new DummyAnimal();
Dog.prototype.constructor = Dog;

他的原始方法是下面的 object:

JavaScript

function object(o) { function F() {} F.prototype = o; return new F(); }
Dog.prototype = object(Animal.prototype); Dog.prototype.constructor =
Dog;

1
2
3
4
5
6
7
8
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}
 
Dog.prototype = object(Animal.prototype);
Dog.prototype.constructor = Dog;

我们可以看到,这就是一个对象Animal,该对象有个属性name,有个方法setName。要注意,一旦修改prototype,比如增加某个方法,则该对象所有实例将同享这个方法。例如

4.4 利用 __proto__ 实现继承

现在就只剩下一个问题了,如何把冗余属性和方法去掉?

其实,从第 3 小节介绍原型的时候就提到了 __proto__ 属性,instanceof
运算符是通过它来判断是否属于某个类型的。

所以我们可以这么继承:

JavaScript

function Dog() { Animal.call(this); } Dog.prototype = { __proto__:
Animal.prototype, constructor: Dog };

1
2
3
4
5
6
7
8
function Dog() {
  Animal.call(this);
}
 
Dog.prototype = {
  __proto__: Animal.prototype,
  constructor: Dog
};

如果不考虑兼容性的话,这应该是从 OO 的角度来看最贴切的继承方式了。

复制代码 代码如下:

4.5 拷贝继承

这个方式也只能称之为 extend 而不是 inherit,所以也没必要展开说。

像 Backbone.Model.extend、jQuery.extend 或者 _.extend
都是拷贝继承,可以稍微看一下它们是怎么实现的。(或者等我自己再好好研究之后过来把这部分补上吧)

function Animal(name) {
    this.name = name;
}
var animal = new Animal(“wangwang”);

5. 个人小结

当我们在讨论继承的实现方式时,给我的感觉就像孔乙己在炫耀“茴香豆”的“茴”有几种写法一样。继承是
JS
中占比很大的一块内容,所以很多库都有自己的实现方式,它们并没有使用我认为的“最贴切”的方法,为什么?JS
就是 JS,它生来就设计得非常灵活,所以我们为什么不利用这个特性,而非得将
OO 的做法强加于它呢?

通过继承,我们更多的是希望获得父类的属性和方法,至于是否要保证严格的父类/子类关系,很多时候并不在乎,而拷贝继承最能体现这一点。对于基于原型的继承,会在代码中看到各种用
function
定义的类型,而拷贝继承更通用,它只是将一个对象的属性和方法拷贝(扩展)到另一个对象而已,并不关心原型链是什么。

当然,在我鼓吹拷贝继承多么多么好时,基于原型的继承自然有它不可取代的理由。所以具体问题得具体分析,当具体的使用场景没定下来时,就不存在最好的方法。

个人见解,能帮助大家更加理解继承一点就最好,如果有什么不对的,请多多指教!

1 赞 4 收藏
评论

www.bifa88.com 6

这时animal只有name属性。如果我们加上一句,

复制代码 代码如下:

Animal.prototype.setName = function(name) {
    this.name = name;
}

这时animal也会有setName方法。

继承本复制——从空的对象开始我们知道,JS的基本类型中,有一种叫做object,而它的最基本实例就是空的对象,即直接调用new
Object()生成的实例,或者是用字面量{
}来声明。空的对象是“干净的对象”,只有预定义的属性和方法,而其他所有对象都是继承自空对象,因此所有的对象都拥有这些预定义的
属性与方法。原型其实也是一个对象实例。原型的含义是指:如果构造器有一个原型对象A,则由该构造器创建的实例都必然复制自A。由于实例复制自对象A,所以实例必然继承了A的所有属性、方法和其他性质。那么,复制又是怎么实现的呢?方法一:构造复制每构造一个实例,都从原型中复制出一个实例来,新的实例与原型占用了相同的内存空间。这虽然使得obj1、obj2与它们的原型“完全一致”,但也非常不经济——内存空间的消耗会急速增加。如图:

www.bifa88.com 7

方法二:写时复制这种策略来自于一致欺骗系统的技术:写时复制。这种欺骗的典型示例就是操作系统中的动态链接库(DDL),它的内存区总是写时复制的。如图:

www.bifa88.com 8

我们只要在系统中指明obj1和obj2等同于它们的原型,这样在读取的时候,只需要顺着指示去读原型即可。当需要写对象(例如obj2)的属性时,我们就复制一个原型的映像出来,并使以后的操作指向该映像即可。如图:

www.bifa88.com 9

这种方式的优点是我们在创建实例和读属性的时候不需要大量内存开销,只在第一次写的时候会用一些代码来分配内存,并带来一些代码和内存上的开销。但此后就不再有这种开销了,因为访问映像和访问原型的效率是一致的。不过,对于经常进行写操作的系统来说,这种方法并不比上一种方法经济。方法三:读遍历这种方法把复制的粒度从原型变成了成员。这种方法的特点是:仅当写某个实例的成员,将成员的信息复制到实例映像中。当写对象属性时,例如(obj2.value=10)时,会产生一个名为value的属性值,放在obj2对象的成员列表中。看图:www.bifa88.com 10

可以发现,obj2仍然是一个指向原型的引用,在操作过程中也没有与原型相同大小的对象实例创建出来。这样,写操作并不导致大量的内存分配,因此内存的使用上就显得经济了。不同的是,obj2(以及所有的对象实例)需要维护一张成员列表。这个成员列表遵循两条规则:保证在读取时首先被访问到如果在对象中没有指定属性,则尝试遍历对象的整个原型链,直到原型为空或或找到该属性。原型链后面会讲。显然,三种方法中,读遍历是性能最优的。所以,JavaScript的原型继承是读遍历的。constructor熟悉C++的人看完最上面的对象的代码,肯定会疑惑。没有class关键字还好理解,毕竟有function关键字,关键字不一样而已。但是,构造函数呢?实际上,JavaScript也是有类似的构造函数的,只不过叫做构造器。在使用new运算符的时候,其实已经调用了构造器,并将this绑定为对象。例如,我们用以下的代码

复制代码 代码如下:

var animal = Animal(“wangwang”);

animal将是undefined。有人会说,没有返回值当然是undefined。那如果将Animal的对象定义改一下:

复制代码 代码如下:

function Animal(name) {
    this.name = name;
    return this;
}

猜猜现在animal是什么?
此时的animal变成window了,不同之处在于扩展了window,使得window有了name属性。这是因为this在没有指定的情况下,默认指向window,也即最顶层变量。只有调用new关键字,才能正确调用构造器。那么,如何避免用的人漏掉new关键字呢?我们可以做点小修改:

复制代码 代码如下:

function Animal(name) {
    if(!(this instanceof Animal)) {
        return new Animal(name);
    }
    this.name = name;
}

这样就万无一失了。构造器还有一个用处,标明实例是属于哪个对象的。我们可以用instanceof来判断,但instanceof在继承的时候对祖先对象跟真正对象都会返回true,所以不太适合。constructor在new调用时,默认指向当前对象。

复制代码 代码如下:

console.log(Animal.prototype.constructor === Animal); // true

我们可以换种思维:prototype在函数初始时根本是无值的,实现上可能是下面的逻辑

// 设定__proto__是函数内置的成员,get_prototyoe()是它的方法

复制代码 代码如下:

var __proto__ = null;
function get_prototype() {
    if(!__proto__) {
        __proto__ = new Object();
        __proto__.constructor = this;
    }
    return __proto__;
}

这样的好处是避免了每声明一个函数都创建一个对象实例,节省了开销。constructor是可以修改的,后面会讲到。基于原型的继承继承是什么相信大家都差不多知道,就不秀智商下限了。

JS的继承有好几种,这里讲两种

  1. 方法一这种方法最常用,安全性也比较好。我们先定义两个对象

复制代码 代码如下:

function Animal(name) {
    this.name = name;
}
function Dog(age) {
    this.age = age;
}
var dog = new Dog(2);

要构造继承很简单,将子对象的原型指向父对象的实例(注意是实例,不是对象)

复制代码 代码如下:

Dog.prototype = new Animal(“wangwang”);

这时,dog就将有两个属性,name和age。而如果对dog使用instanceof操作符

复制代码 代码如下:

console.log(dog instanceof Animal); // true
console.log(dog instanceof Dog); // false

这样就实现了继承,但是有个小问题

复制代码 代码如下:

console.log(Dog.prototype.constructor === Animal); // true
console.log(Dog.prototype.constructor === Dog); // false

可以看到构造器指向的对象更改了,这样就不符合我们的目的了,我们无法判断我们new出来的实例属于谁。因此,我们可以加一句话:

复制代码 代码如下:

Dog.prototype.constructor = Dog;

再来看一下:

复制代码 代码如下:

console.log(dog instanceof Animal); // false
console.log(dog instanceof Dog); // true

done。这种方法是属于原型链的维护中的一环,下文将详细阐述。2.
方法二这种方法有它的好处,也有它的弊端,但弊大于利。先看代码

复制代码 代码如下:

<pre name=”code” class=”javascript”>function Animal(name) {
    this.name = name;
}
Animal.prototype.setName = function(name) {
    this.name = name;
}
function Dog(age) {
    this.age = age;
}
Dog.prototype = Animal.prototype;

这样就实现了prototype的拷贝。

这种方法的好处就是不需要实例化对象(和方法一相比),节省了资源。弊端也是明显,除了和上文一样的问题,即constructor指向了父对象,还只能复制父对象用prototype声明的属性和方法。也即是说,上述代码中,Animal对象的name属性得不到复制,但能复制setName方法。最最致命的是,对子对象的prototype的任何修改,都会影响父对象的prototype,也就是两个对象声明出来的实例都会受到影响。所以,不推荐这种方法。

原型链

写过继承的人都知道,继承可以多层继承。而在JS中,这种就构成了原型链。上文也多次提到了原型链,那么,原型链是什么?一个实例,至少应该拥有指向原型的proto属性,这是JavaScript中的对象系统的基础。不过这个属性是不可见的,我们称之为“内部原型链”,以便和构造器的prototype所组成的“构造器原型链”(亦即我们通常所说的“原型链”)区分开。我们先按上述代码构造一个简单的继承关系:

复制代码 代码如下:

function Animal(name) {
    this.name = name;
}
function Dog(age) {
    this.age = age;
}
var animal = new Animal(“wangwang”);
Dog.prototype = animal;
var dog = new Dog(2);

提醒一下,前文说过,所有对象都是继承空的对象的。所以,我们就构造了一个原型链:

www.bifa88.com 11

我们可以看到,子对象的prototype指向父对象的实例,构成了构造器原型链。子实例的内部proto对象也是指向父对象的实例,构成了内部原型链。当我们需要寻找某个属性的时候,代码类似于

复制代码 代码如下:

function getAttrFromObj(attr, obj) {
    if(typeof(obj) === “object”) {
        var proto = obj;
        while(proto) {
            if(proto.hasOwnProperty(attr)) {
                return proto[attr];
            }
            proto = proto.__proto__;
        }
    }
    return undefined;
}

在这个例子中,我们如果在dog中查找name属性,它将在dog中的成员列表中寻找,当然,会找不到,因为现在dog的成员列表只有age这一项。接着它会顺着原型链,即.proto指向的实例继续寻找,即animal中,找到了name属性,并将之返回。假如寻找的是一个不存在的属性,在animal中寻找不到时,它会继续顺着.proto寻找,找到了空的对象,找不到之后继续顺着.proto寻找,而空的对象的.proto指向null,寻找退出。

原型链的维护我们在刚才讲原型继承的时候提出了一个问题,使用方法一构造继承时,子对象实例的constructor指向的是父对象。这样的好处是我们可以通过constructor属性来访问原型链,坏处也是显而易见的。一个对象,它产生的实例应该指向它本身,也即是

复制代码 代码如下:

(new obj()).prototype.constructor === obj;

然后,当我们重写了原型属性之后,子对象产生的实例的constructor不是指向本身!这样就和构造器的初衷背道而驰了。我们在上面提到了一个解决方案:

复制代码 代码如下:

Dog.prototype = new Animal(“wangwang”);
Dog.prototype.constructor = Dog;

看起来没有什么问题了。但实际上,这又带来了一个新的问题,因为我们会发现,我们没法回溯原型链了,因为我们没法寻找到父对象,而内部原型链的.proto属性是无法访问的。于是,SpiderMonkey提供了一个改良方案:在任何创建的对象上添加了一个名为__proto__的属性,该属性总是指向构造器所用的原型。这样,对任何constructor的修改,都不会影响__proto__的值,就方便维护constructor了。

但是,这样又两个问题:

__proto__是可以重写的,这意味着使用它时仍然有风险

__proto__是spiderMonkey的特殊处理,在别的引擎(例如JScript)中是无法使用的。

我们还有一种办法,那就是保持原型的构造器属性,而在子类构造器函数内初始化实例的构造器属性。

代码如下:改写子对象

复制代码 代码如下:

function Dog(age) {
    this.constructor = arguments.callee;
    this.age = age;
}
Dog.prototype = new Animal(“wangwang”);

这样,所有子对象的实例的constructor都正确的指向该对象,而原型的constructor则指向父对象。虽然这种方法的效率比较低,因为每次构造实例都要重写constructor属性,但毫无疑问这种方法能有效解决之前的矛盾。ES5考虑到了这种情况,彻底的解决了这个问题:可以在任意时候使用Object.getPrototypeOf()
来获得一个对象的真实原型,而无须访问构造器或维护外部的原型链。因此,像上一节所说的寻找对象属性,我们可以如下改写:

复制代码 代码如下:

function getAttrFromObj(attr, obj) {
    if(typeof(obj) === “object”) {
        do {
            var proto = Object.getPrototypeOf(dog);
            if(proto[attr]) {
                return proto[attr];
            }
        }
        while(proto);
    }
    return undefined;
}

当然,这种方法只能在支持ES5的浏览器中使用。为了向后兼容,我们还是需要考虑上一种方法的。更合适的方法是将这两种方法整合封装起来,这个相信读者们都非常擅长,这里就不献丑了。

您可能感兴趣的文章:

  • JavaScript继承与多继承实例分析
  • JavaScript实现多重继承的方法分析
  • 深入浅析javascript继承体系
  • JS继承与闭包及JS实现继承的三种方式
  • js中继承的几种用法总结(apply,call,prototype)
  • JavaScript是如何实现继承的(六种方式)
  • 深入了解javascript中的prototype与继承
  • Javascript基于对象三大特性(封装性、继承性、多态性)
  • javascript的函数、创建对象、封装、属性和方法、继承
  • Javascript 继承机制的实现
  • JavaScript继承定义与用法实践分析