原型

一、了解自有属性

  • hasOwnProperty

下面的实例,Bird 构造函数定义了两个属性:name 和 numLegs:

1
2
3
4
5
6
7
8

function Bird(name) {
  this.name  = name;
  this.numLegs = 2;
}

let duck = new Bird("Donald");
let canary = new Bird("Tweety");

name 和 numLegs 被叫做 自身属性,因为它们是直接在实例对象上定义的。

这就意味着 duck 和 canary 这两个对象分别拥有这些属性的独立副本

事实上,Bird 的所有实例都将拥有这些属性的独立副本。

下面的代码将 duck 的所有自身属性都存到一个叫作 ownProps 的数组里面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

let ownProps = [];

for (let property in duck) {
  if(duck.hasOwnProperty(property)) {
    ownProps.push(property);
  }
}

console.log(ownProps); // ["name", "numLegs"]

二、使用原型属性来减少重复代码

所有 Bird 实例可能会有相同的 numLegs 值,
所以在每一个 Bird 的实例中本质上都有一个重复的变量 numLegs。

当只有两个实例时可能并不是什么问题,但想象一下如果有数百万个实例。 这将会产生许许多多重复的变量。

更好的方法是使用 Bird 的 prototype

prototype 是一个可以在所有 Bird 实例之间共享的对象。
以下是一个在 Bird prototype 中添加 numLegs 属性的示例:

1
2

Bird.prototype.numLegs = 2;

现在所有的 Bird 实例都拥有了共同的 numLegs 属性值。

1
2
3

console.log(duck.numLegs); // 2
console.log(canary.numLegs); // 2

由于所有的实例都可以继承 prototype 上的属性,所以可以把 prototype 看作是创建对象的 “配方”。

请注意:duck 和 canary 的 prototype 属于 Bird 的构造函数,即 Bird 的原型 Bird.prototype。

JavaScript 中几乎所有的对象都有一个 prototype 属性,这个属性是属于它所在的构造函数。


三、迭代所有属性

现在已经了解了两种属性: 自身属性prototype 属性。

自身属性是直接在对象上定义的。 而原型属性在 prototype 上定义。

1
2
3
4
5
6
7
8

function Bird(name) {
  this.name = name;  //own property
}

Bird.prototype.numLegs = 2; // prototype property

let duck = new Bird("Donald");

这个示例会告诉你如何将 duck 的 自身属性 和 prototype 属性分别添加到 ownProps 数组和 prototypeProps 数组里面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

let ownProps = [];
let prototypeProps = [];

for (let property in duck) {
  if(duck.hasOwnProperty(property)) {
    ownProps.push(property);
  } else {
    prototypeProps.push(property);
  }
}

console.log(ownProps); // [ 'name' ]
console.log(prototypeProps); // [ 'numLegs' ]

四、了解构造函数属性

上例创建的实例对象 duck 和 beagle 都有一个特殊constructor 属性:

1
2
3
4
5
6

let duck = new Bird();
let beagle = new Dog();

console.log(duck.constructor === Bird); // true
console.log(beagle.constructor === Dog); // true

需要注意到的是这个 constructor 属性是对创建这个实例的构造函数的一个引用。

constructor 属性的一个好处是可以通过检查这个属性来找出它是一个什么对象。

下面是一个例子,来看看是怎么使用的:

1
2
3
4
5
6
7
8

function joinBirdFraternity(candidate) {
  if (candidate.constructor === Bird) {
    return true;
  } else {
    return false;
  }
}

注意: 由于 constructor 属性可以被重写,所以最好使用 instanceof 方法来检查对象的类型。


五、将原型更改为新对象

单独给 prototype 添加属性:

1
2

Bird.prototype.numLegs = 2;

需要添加多个属性的,这未免会显得拖沓

1
2
3
4
5
6
7
8

Bird.prototype.eat = function() {
  console.log("nom nom nom");
}

Bird.prototype.describe = function() {
  console.log("My name is " + this.name);
}

一种更有效的方法就是给对象的 prototype 设置为一个已经包含了属性的新对象

这样一来,所有属性都可以一次性添加进来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

Bird.prototype = {
  numLegs: 2, 
  eat: function() {
    console.log("nom nom nom");
  },
  describe: function() {
    console.log("My name is " + this.name);
  }
};

六、更改原型时,记得设置构造函数属性

手动设置一个新对象的原型有一个重要的副作用。

它清除了 constructor 属性!

此属性可以用来检查是哪个构造函数创建了实例,但由于该属性已被覆盖,它现在给出了错误的结果:

1
2
3
4

duck.constructor === Bird; // false
duck.constructor === Object; // true
duck instanceof Bird; // true

为了解决这个问题,凡是手动给新对象重新设置过原型对象的,都别忘记在原型对象中定义一个 constructor 属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

Bird.prototype = {
  constructor: Bird,
  numLegs: 2,
  eat: function() {
    console.log("nom nom nom");
  },
  describe: function() {
    console.log("My name is " + this.name); 
  }
};

七、了解对象的原型来自哪里

就像人们从父母那里继承基因一样,对象也可直接从创建它的构造函数那里继承其 prototype。

请看下面的例子:Bird 构造函数创建了一个 duck 对象:

1
2
3
4
5
6

function Bird(name) {
  this.name = name;
}

let duck = new Bird("Donald");

duck 从 Bird 构造函数那里继承了它的 prototype。

你可以使用 isPrototypeOf 方法来验证他们之间的关系:

1
2

Bird.prototype.isPrototypeOf(duck); // true

八、了解原型链

JavaScript 中所有的对象(除了少数例外)都有自己的 prototype。

而且,对象的 prototype 本身也是一个对象。

1
2
3
4
5
6

function Bird(name) {
  this.name = name;
}

typeof Bird.prototype; // object

正因为 prototype 是一个对象,所以 prototype 对象也有它自己的 prototype!

这样看来的话,Bird.prototype 的 prototype 就是 Object.prototype:

1
2

Object.prototype.isPrototypeOf(Bird.prototype); // true

这有什么作用呢?

1
2
3

let duck = new Bird("Donald");
duck.hasOwnProperty("name");

hasOwnProperty 是定义在 Object.prototype 上的一个方法
尽管在 Bird.prototype 和 duck上并没有定义该方法,但是我们依然可以在这两个对象上访问到。

这就是 prototype 链的一个例子。

在这个 prototype 链中,Bird 是 duck 的 supertype,而 duck 是 subtype。

Object 则是 Bird 和 duck 实例共同的 supertype。

Object 是 JavaScript 中所有对象supertype,也就是原型链的最顶层

因此,所有对象都可以访问 hasOwnProperty 方法。


十、使用继承避免重复

有一条原则叫做:Don’t Repeat Yourself。

常以缩写形式 DRY 出现,意思是“不要自己重复”。

编写重复代码会产生的问题是:任何改变都需要去多个地方修复所有重复的代码。

这通常意味着我们需要做更多的工作,会产生更高的出错率。

请观察下面的示例,Bird 和 Dog 共享 describe 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

Bird.prototype = {
  constructor: Bird,
  describe: function() {
    console.log("My name is " + this.name);
  }
};

Dog.prototype = {
  constructor: Dog,
  describe: function() {
    console.log("My name is " + this.name);
  }
};

我们可以看到 describe 方法在两个地方重复定义了。

根据以上所说的 DRY 原则,我们可以通过创建一个 Animal supertype(或者父类)来重写这段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

function Animal(name) { };

Animal.prototype = {
  constructor: Animal, 
  name: name,
  describe: function() {
    console.log("My name is " + this.name);
  }
};

Animal 构造函数中定义了 describe 方法,可将 Bird 和 Dog 这两个构造函数的方法删除掉:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

let Bird = new Animal()
let Dog = new Animal()

Bird.prototype = {
  constructor: Bird
};

Dog.prototype = {
  constructor: Dog
};

十一、从超类继承行为

创建一个超类 supertype(或者叫父类)的实例

1
2
3
4
5

function Animal() { }
Animal.prototype.eat = function() {
  console.log("nom nom nom");
};
1
2

let animal = new Animal();

此语法用于继承时会存在一些缺点,太复杂了animal.prototype = {constructor: animal}

相反,另外一种没有这些缺点的方法来替代 new 操作:

1
2

let animal = Object.create(Animal.prototype);

Object.create(obj) 创建了一个新对象,并指定了 obj 作为新对象的 prototype。

回忆一下,之前说过 prototype 就像是创建对象的“配方”。

如果把 animal 的 prototype 设置为与 Animal 构造函数的 prototype 一样,
那么就相当于让 animal 这个实例具有与 Animal 的其他实例相同的“配方”了。

1
2
3

animal.eat(); // nom nom nom
animal instanceof Animal; // true

十二、将子辈的原型设置为父辈的实例

给子类型(或者子类)设置 prototype。 这样一来,Bird 就是 Animal 的一个实例了

1
2

Bird.prototype = Object.create(Animal.prototype);

请记住,prototype 类似于创建对象的“配方”。 从某种意义上来说,Bird 对象的配方包含了 Animal 的所有关键“成分”。

1
2
3

let duck = new Bird("Donald");
duck.eat();

duck 继承了Animal 的所有属性,其中包括了 eat 方法。


十三、重置一个继承的构造函数属性

当一个对象从另一个对象那里继承了其 prototype 时,那它也继承了父类的 constructor 属性。

1
2
3
4
5

function Bird() { }
Bird.prototype = Object.create(Animal.prototype);
let duck = new Bird();
duck.constructor

但是 duck 和其他所有 Bird 的实例都应该表明它们是由 Bird 创建的,而不是由 Animal 创建的。

为此,可以手动将 Bird 的构造函数属性设置为 Bird 对象:

1
2
3
4

Bird.prototype.constructor = Bird;
let duck = new Bird();
duck.constructor // [Function: Bird]

十四、继承后添加方法

从超类构造函数继承其 prototype 对象的构造函数,除了继承的方法外,还可以拥有自己的方法。

Bird 是一个构造函数,它继承了 Animal 的 prototype:

1
2
3
4
5
6
7
8

function Animal() { }
Animal.prototype.eat = function() {
  console.log("nom nom nom");
};
function Bird() { }
Bird.prototype = Object.create(Animal.prototype); // {}
Bird.prototype.constructor = Bird; // [Function: Bird]

除了从 Animal 构造函数继承的行为之外,还需要给 Bird 对象添加它独有的行为。

这里,我们给 Bird 对象添加一个 fly() 函数。

函数会以一种与其他构造函数相同的方式添加到 Bird’s 的 prototype 中:

1
2
3
4

Bird.prototype.fly = function() {
  console.log("I'm flying!");
};

现在 Bird 的实例中就有了 eat() 和 fly() 这两个方法:

1
2
3
4

let duck = new Bird();
duck.eat(); // nom nom nom
duck.fly(); // I'm flying!

十五、重写继承方法

通过使用一个与需要重写的方法相同的方法名,向 ChildObject.prototype 中添加方法。

请看下面的举例:Bird 重写了从 Animal 继承来的 eat() 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

function Animal() { }
Animal.prototype.eat = function() {
  return "nom nom nom";
};
function Bird() { }

Bird.prototype = Object.create(Animal.prototype);

Bird.prototype.eat = function() {
  return "peck peck peck";
};

如果你有一个实例:let duck = new Bird();,然后你调用了 duck.eat(),以下就是 JavaScript 在 duck 的 prototype 链上寻找方法的过程:

  1. duck => eat() 是定义在这里吗? 不是。
  2. Bird => eat() 是定义在这里吗? => 是的。 执行它并停止往上搜索。
  3. Animal => 这里也定义了 eat() 方法,但是 JavaScript 在到达这层原型链之前已停止了搜索。
  4. Object => JavaScript 在到达这层原型链之前也已经停止了搜索。

十六、使用 Mixin 在不相关对象之间添加共同行为

行为是可以通过继承来共享的。

然而,在有些情况下,继承不是最好的解决方案。

继承不适用于不相关的对象,比如 Bird 和 Airplane。
虽然它们都可以飞行,但是 Bird 并不是一种 Airplane,反之亦然。

对于不相关的对象,更好的方法是使用 mixins

mixin 允许其他对象使用函数集合。

1
2
3
4
5
6

let flyMixin = function(obj) {
  obj.fly = function() {
    console.log("Flying, wooosh!");
  }
};

flyMixin 能接受任何对象,并为其提供 fly 方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

let bird = {
  name: "Donald",
  numLegs: 2
};

let plane = {
  model: "777",
  numPassengers: 524
};

flyMixin(bird);
flyMixin(plane);

这里的 flyMixin 接收了bird 和 plane 对象,然后将 fly 方法分配给了每一个对象。 现在 bird 和 plane 都可以飞行了:

1
2
3

bird.fly(); // Flying, wooosh!
plane.fly(); // Flying, wooosh!

十七、使用闭包保护对象内的属性不被外部修改

bird 有一个公共属性 name。 公共属性的定义就是:它可以在 bird 的定义范围之外被访问和更改。

1
2

bird.name = "Duffy";

因此,代码的任何地方都可以轻松地将 bird 的 name 属性更改为任意值。

想想密码和银行账户之类的东西,如果代码库的任何部分都可以轻易改变他们。 那么将会引起很多问题。

使属性私有化最简单的方法就是在构造函数中创建变量。

可以将该变量范围限定在构造函数中,而不是全局可用。

这样,属性只能由构造函数中的方法访问和更改。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

function Bird() {
  let hatchedEgg = 10;

  this.getHatchedEggCount = function() { 
    return hatchedEgg;
  };
}
let ducky = new Bird();
ducky.getHatchedEggCount();
console.log(ducky.hatcheEgg) // undefined

这里的 getHatchedEggCount 是一种特权方法,因为它可以访问私有属性 hatchedEgg。

这是因为 hatchedEgg 是在与 getHatchedEggCount 相同的上下文中声明的。

在 JavaScript 中,函数总是可以访问创建它的上下文。 这就叫做 闭包 closure


知识点

  • hasOwnProperty 获取自身属性的方法

  • prototype 原型属性

  • isPrototypeOf 验证原型的关系的方法

  • 如果不用传参: Object.create(object) 优于 new 关键字

  • constructor

  • closure 闭包