《JavaScript 闯关记》之原型及原型链

原型链是一种体制,指的是 JavaScript 每个对象都有一个内置的 __proto__
属性指向制造它的构造函数的
prototype(原型)属性。原型链的效果是为了贯彻目的的后续,要精通原型链,需要先从函数对象constructornewprototype__proto__
这六个概念出手。

函数对象

前方讲过,在 JavaScript
里,函数即对象,程序可以擅自操控它们。比如,可以把函数赋值给变量,或者当作参数传递给其他函数,也足以给它们设置属性,甚至调用它们的法子。下边示例代码对「普通对象」和「函数对象」举行了分别。

普普通通对象:

var o1 = {};
var o2 = new Object();

函数对象:

function f1(){};
var f2 = function(){};
var f3 = new Function('str','console.log(str)');

简单来说的说,凡是利用 function 关键字或 Function
构造函数成立的靶子都是函数对象。而且,只有函数对象才具备 prototype
(原型)属性。

constructor 构造函数

函数还有一种用法,就是把它当作构造函数使用。像 ObjectArray
这样的原生构造函数,在运作时会自动出现在实行环境中。另外,也得以创建自定义的构造函数,从而自定义对象类型的性能和章程。如下代码所示:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        console.log(this.name);
    };
}

var person1 = new Person("Stone", 28, "Software Engineer");
var person2 = new Person("Sophie", 29, "English Teacher");

在这么些例子中,大家创立了一个自定义构造函数
Person(),并通过该构造函数成立了五个平日对象 person1
person2,这五个常见对象均隐含3个属性和1个点子。

您应当小心到函数名 Person 使用的是大写字母
P。遵照惯例,构造函数始终都应当以一个大写字母起初,而非构造函数则应该以一个小写字母开首。这些做法借鉴自其他面向对象语言,重如果为了区别于
JavaScript
中的其他函数;因为构造函数本身也是函数,只可是可以用来创设对象而已。

new 操作符

要创建 Person 的新实例,必须采取 new
操作符。以这种格局调用构造函数实际上会经历以下4个步骤:

  1. 始建一个新目的;
  2. 将构造函数的功力域赋给新目标(因而 this 就针对了这一个新目标);
  3. 实施构造函数中的代码(为这些新对象添加属性);
  4. 回到新对象。

将构造函数当作函数

构造函数与其他函数的唯一区别,就在于调用它们的不二法门不同。可是,构造函数毕竟也是函数,不设有定义构造函数的非正规语法。任何函数,只要经过
new 操作符来调用,这它就足以看做构造函数;而其它函数,倘使不经过 new
操作符来调用,这它跟通常函数也不会有什么不同。例如,后面例子中定义的
Person() 函数可以通过下列任何一种办法来调用。

// 当作构造函数使用
var person = new Person("Stone", 28, "Software Engineer");
person.sayName(); // "Stone"

// 作为普通函数调用
Person("Sophie", 29, "English Teacher"); // 添加到 window
window.sayName(); // "Sophie"

// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Tommy", 3, "Baby");
o.sayName(); // "Tommy"

本条例子中的前两行代码映现了构造函数的第一名用法,即使用 new
操作符来创立一个新对象。接下来的两行代码体现了不选取 new 操作符调用
Person() 会出现哪些结果,属性和章程都被添加给 window
对象了。当在大局效用域中调用一个函数时,this 对象总是指向 Global
对象(在浏览器中就是 window 对象)。因而,在调用完函数之后,可以由此
window 对象来调用 sayName() 方法,并且还回去了 "Sophie"
。末了,也得以应用 call()(或者
apply())在某个特殊目的的功用域中调用 Person() 函数。这里是在目的
o 的效能域中调用的,由此调用后 o 就拥有了装有属性和 sayName()
方法。

构造函数的问题

构造函数格局即使好用,但也并非没有缺陷。使用构造函数的要害问题,就是每个方法都要在每个实例上再一次创立四次。在眼前的事例中,person1
person2 都有一个名为 sayName() 的章程,但这六个法子不是同一个
Function 的实例。因为 JavaScript
中的函数是目的,由此每定义一个函数,也就是实例化了一个对象。从逻辑角度讲,此时的构造函数也得以这么定义。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("console.log(this.name)"); // 与声明函数在逻辑上是等价的
}

从那些角度上来看构造函数,更易于精晓每个 Person 实例都富含一个例外的
Function 实例(sayName()
方法)。说得明白些,以那种方法创制函数,即使创建 Function
新实例的编制依然是平等的,不过不同实例上的同名函数是不对等的,以下代码可以证实这或多或少。

console.log(person1.sayName == person2.sayName);  // false

不过,创制六个到位同样任务的 Function 实例的确没有必要;况且有 this
对象在,根本不用在实施代码前就把函数绑定到一定目标方面。由此,大可像下边这样,通过把函数定义转移到构造函数外部来化解这多少个题目。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}

function sayName(){
    console.log(this.name);
}

var person1 = new Person("Stone", 28, "Software Engineer");
var person2 = new Person("Sophie", 29, "English Teacher");

在这一个例子中,我们把 sayName()
函数的概念转移到了构造函数外部。而在构造函数内部,我们将 sayName
属性设置成等于全局的 sayName 函数。这样一来,由于 sayName
包含的是一个对准函数的指针,由此 person1person2
对象就共享了在全局功效域中定义的同一个 sayName()
函数。这样做实在解决了两个函数做同样件事的题目,可是新题材又来了,在全局功效域中定义的函数实际上只能被某个对象调用,这让全局功能域有点名不副实。而更令人不能承受的是,假诺目的急需定义很多情势,那么就要定义很两个全局函数,于是大家以此自定义的引用类型就丝毫不曾封装性可言了。好在,这多少个问题得以经过行使原型来解决。

prototype 原型

咱俩创制的各样函数都有一个
prototype(原型)属性。使用原型的利益是可以让抱有目的实例共享它所蕴藏的性质和情势。换句话说,不必在构造函数中定义对象实例的音信,而是可以将这一个音信向来抬高到原型中,如下边的例证所示。

function Person(){}

Person.prototype.name = "Stone";
Person.prototype.age = 28;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.sayName();   // "Stone"

var person2 = new Person();
person2.sayName();   // "Stone"

console.log(person1.sayName == person2.sayName);  // true

在此,我们将 sayName() 方法和兼具属性直接助长到了 Person
prototype
属性中,构造函数变成了空函数。即便这样,也照样可以因此调用构造函数来创建新目标,而且新对象还会拥有同样的特性和模式。但与前边的事例不同的是,新对象的这么些属性和办法是由所有实例共享的。换句话说,person1
person2 访问的都是如出一辙组属性和同一个 sayName() 函数。

略知一二原型对象

在默认情况下,所有原型对象都会自行得到一个
constructor(构造函数)属性,这些特性包含一个指向 prototype
属性所在函数的指针。就拿前边的例子来说,Person.prototype.constructor
指向
Person。而经过这个构造函数,我们还可连续为原型对象添加其余属性和艺术。

即使可以因而对象实例访问保存在原型中的值,但却不可能通过对象实例重写原型中的值。如若大家在实例中添加了一个特性,而该属性与实例原型中的一个性能同名,这大家就在实例中开创该属性,该属性将会遮掩原型中的这多少个属性。来看下边的事例。

function Person(){}

Person.prototype.name = "Stone";
Person.prototype.age = 28;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

person1.name = "Sophie";
console.log(person1.name);     // "Sophie",来自实例
console.log(person2.name);     // "Stone",来自原型

在这么些例子中,person1name 被一个新值给挡住了。但无论访问
person1.name 依旧访问 person2.name 都可以正常地重回值,即分别是
"Sophie"(来自目标实例)和 "Stone"(来自原型)。当访问
person1.name 时,需要读取它的值,因而就会在这一个实例上搜索一个名为
name
的习性。这么些特性确实存在,于是就回去它的值而不要再寻找原型了。当访问
person2. name
时,并没有在实例上发现该属性,因此就会持续搜寻原型,结果在这里找到了
name 属性。

当为对象实例添加一个属性时,这么些特性就会屏蔽原型中保留的同名属性;换句话说,添加这一个特性只会阻碍我们走访原型中的这么些属性,但不会修改分外属性。虽然将以此特性设置为
null
,也只会在实例中设置这多少个特性,而不会还原其针对性原型的连续。然则,使用
delete
操作符则足以完全除去实例属性,从而让我们可以再度访问原型中的属性,如下所示。

function Person(){}

Person.prototype.name = "Stone";
Person.prototype.age = 28;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

person1.name = "Sophie";
console.log(person1.name);     // "Sophie",来自实例
console.log(person2.name);     // "Stone",来自原型

delete person1.name;
console.log(person1.name);     // "Stone",来自原型

在这么些修改后的例子中,大家运用 delete 操作符删除了
person1.name,此前它保存的 "Sophie"
值屏蔽了同名的原型属性。把它删除未来,就复苏了对原型中 name
属性的连续。因而,接下去再调用 person1.name 时,再次回到的就是原型中
name 属性的值了。

更简便的原型语法

前方例子中每添加一个属性和方法就要敲一回
Person.prototype。为压缩不必要的输入,也为了从视觉上更好地卷入原型的效应,更广阔的做法是用一个富含所有属性和办法的目标字面量来重写整个原型对象,如下边的例证所示。

function Person(){}

Person.prototype = {
    name : "Stone",
    age : 28,
    job: "Software Engineer",
    sayName : function () {
        console.log(this.name);
    }
};

在上头的代码中,我们将 Person.prototype
设置为等于一个以目标字面量形式创建的新对象。最后结果一致,但有一个不比:constructor
属性不再指向 Person
了。前边已经介绍过,每创造一个函数,就会同时创设它的 prototype
对象,这几个目的也会自动获取 constructor
属性。而我辈在这边运用的语法,本质上完全重写了默认的 prototype
对象,因此 constructor 属性也就成为了新目标的 constructor 属性(指向
Object 构造函数),不再指向 Person 函数。此时,尽管 instanceof
操作符还是可以重临正确的结果,但经过 constructor
已经力不从心确定目的的品种了,如下所示。

var friend = new Person();

console.log(friend instanceof Object);        // true
console.log(friend instanceof Person);        // true
console.log(friend.constructor === Person);    // false
console.log(friend.constructor === Object);    // true

在此,用 instanceof 操作符测试 ObjectPerson 依旧再次回到
true,但 constructor 属性则等于 Object 而不对等 Person 了。如果
constructor 的值真的很重大,能够像下边这样专门将它设置回适当的值。

function Person(){}

Person.prototype = {
    constructor : Person,
    name : "Stone",
    age : 28,
    job: "Software Engineer",
    sayName : function () {
        console.log(this.name);
    }
};

如上代码特意包含了一个 constructor 属性,并将它的值设置为 Person
,从而确保了经过该属性可以访问到适合的值。

只顾,以这种措施重设 constructor 属性会招致它的 [[Enumerable]]
特性被设置为 true。默认情形下,原生的 constructor
属性是不可枚举的,因而假诺您利用卓越 ECMAScript 5 的 JavaScript
引擎,可以试一试 Object.defineProperty()

function Person(){}

Person.prototype = {
    name : "Stone",
    age : 28,
    job : "Software Engineer",
    sayName : function () {
        console.log(this.name);
    }
}; 

// 重设构造函数,只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
});

原型的动态性

鉴于在原型中查找值的进程是五遍搜索,由此我们对原型对象所做的任何修改都可以立即从实例上反映出去,尽管是先创制了实例后修改原型也依旧如此。请看下边的事例。

var friend = new Person();

Person.prototype.sayHi = function(){
    console.log("hi");
};

friend.sayHi();   // "hi"(没有问题!)

上述代码先创制了 Person 的一个实例,并将其保存在 friend
中。然后,下一条语句在 Person.prototype 中添加了一个格局
sayHi()。即使 person
实例是在充足新章程从前成立的,但它仍然可以访问这多少个新模式。其原因能够归咎为实例与原型之间的松懈连接关系。当大家调用
friend.sayHi() 时,首先会在实例中找找名为 sayHi
的习性,在没找到的情形下,会继续寻找原型。因为实例与原型之间的连续只但是是一个指南针,而非一个副本,由此就可以在原型中找到新的
sayHi 属性并回到保存在这边的函数。

即便可以随时为原型添加属性和模式,并且修改可以登时在享有目标实例中呈现出来,但假如是重写整个原型对象,那么情状就不平等了。我们清楚,调用构造函数时会为实例添加一个对准最初原型的
[[Prototype]]
指针,而把原型修改为此外一个对象就分外隔绝了构造函数与先前时期原型之间的牵连。请记住:实例中的指针仅针对原型,而不指向构造函数。看上边的事例。

function Person(){}

var friend = new Person();

Person.prototype = {
    constructor: Person,
    name : "Stone",
    age : 28,
    job : "Software Engineer",
    sayName : function () {
        console.log(this.name);
    }
};

friend.sayName();   // Uncaught TypeError: friend.sayName is not a function

在这么些事例中,我们先创立了 Person
的一个实例,然后又重写了其原型对象。然后在调用 friend.sayName()
时发生了不当,因为 friend
指向的是重写前的原型对象,其中并不含有以该名字命名的特性。

原生对象的原型

原型的严重性不仅反映在开创自定义类型方面,就连具有原生的引用类型,都是使用那种格局成立的。所有原生引用类型(ObjectArrayString,等等)都在其构造函数的原型上定义了主意。例如,在
Array.prototype 中可以找到 sort() 方法,而在 String.prototype
中得以找到 substring() 方法,如下所示。

console.log(typeof Array.prototype.sort);       // "function"
console.log(typeof String.prototype.substring); // "function"

通过原生对象的原型,不仅可以获取富有默认方法的引用,而且也可以定义新艺术。可以像修改自定义对象的原型一样修改原生对象的原型,因而能够每日添加方法。下边的代码就给主旨包装档次
String 添加了一个名为 startsWith() 的方法。

String.prototype.startsWith = function (text) {
    return this.indexOf(text) === 0;
};

var msg = "Hello world!";
console.log(msg.startsWith("Hello"));   // true

此间新定义的 startsWith() 方法会在传出的文件位于一个字符串起头时再次来到
true。既然方法被添加给了 String.prototype
,那么当前环境中的所有字符串就都可以调用它。由于 msg
是字符串,而且后台会调用 String 基本包装函数创立这个字符串,由此通过
msg 就足以调用 startsWith() 方法。

尽管可以这么做,但大家不推荐在产品化的主次中修改原生对象的原型。如若因某个实现中紧缺某个方法,就在原生对象的原型中添加那么些措施,那么当在另一个援助该措施的落实中运作代码时,就可能会造成命名争论。而且,这样做也恐怕会奇怪地重写原生方法。

原型对象的题目

原型情势也不是没有缺陷。首先,它大概了为构造函数传递起先化参数这一环节,结果具有实例在默认处境下都将赢得一致的属性值。即使这会在某种程度上带来一些不便利,但还不是原型的最大问题。原型情势的最大题材是由其共享的秉性所造成的。

原型中享有属性是被广大实例共享的,那种共享对于函数非常恰当。对于那一个带有基本值的性能倒也说得过去,毕竟(如前方的事例所示),通过在实例上添加一个同名属性,可以隐蔽原型中的对应属性。然则,对于富含引用类型值的属性来说,问题就相比较出色了。来看下面的事例。

function Person(){}

Person.prototype = {
    constructor: Person,
    name : "Stone",
    age : 28,
    job : "Software Engineer",
    friends : ["ZhangSan", "LiSi"],
    sayName : function () {
        console.log(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push("WangWu");

console.log(person1.friends);    // "ZhangSan,LiSi,WangWu"
console.log(person2.friends);    // "ZhangSan,LiSi,WangWu"
console.log(person1.friends === person2.friends);  // true

在此,Person.prototype 对象有一个名为 friends
的属性,该属性包含一个字符串数组。然后,成立了 Person
的多少个实例。接着,修改了 person1.friends
引用的数组,向数组中添加了一个字符串。由于 friends 数组存在于
Person.prototype 而非 person1 中,所以刚刚提到的修改也会通过
person2.friends(与 person1.friends
指向同一个数组)反映出来。尽管我们的初衷就是像这么在具有实例中共享一个数组,那么对这么些结果自己尚未话可说。不过,实例一般都是要有属于自己的凡事属性的。

构造函数和原型结合

因而,构造函数用于定义实例属性,而原型用于定义方法和共享的性质。结果,每个实例都会有投机的一份实例属性的副本,但与此同时又共享着对章程的引用,最大限度地节约了内存。上边的代码重写了前方的例证。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["ZhangSan", "LiSi"];
}

Person.prototype = {
    constructor : Person,
    sayName : function(){
        console.log(this.name);
    }
}

var person1 = new Person("Stone", 28, "Software Engineer");
var person2 = new Person("Sophie", 29, "English Teacher");

person1.friends.push("WangWu");
console.log(person1.friends);    // "ZhangSan,LiSi,WangWu"
console.log(person2.friends);    // "ZhangSan,LiSi"
console.log(person1.friends === person2.friends);    // false
console.log(person1.sayName === person2.sayName);    // true

在那多少个例子中,实例属性都是在构造函数中定义的,而由拥有实例共享的属性
constructor 和方法 sayName() 则是在原型中定义的。而修改了
person1.friends(向其中添加一个新字符串),并不会潜移默化到
person2.friends,因为它们分别引用了不同的数组。

这种构造函数与原型混成的格局,是现阶段在 JavaScript
中运用最广泛、认可度最高的一种创造自定义类型的艺术。可以说,这是用来定义引用类型的一种默认格局。

__proto__

干什么在构造函数的 prototype
中定义了性能和措施,它的实例中就能访问呢?

这是因为当调用构造函数创立一个新实例后,该实例的内部将涵盖一个指针
__proto__,指向构造函数的原型。Firefox、Safari 和 Chrome
的每个对象上都有这些属性
,而在其余浏览器中是截然不可见的(为了保证浏览器兼容性问题,不要直接采纳
__proto__ 属性,此处只为解释原型链而演示)。让大家来看下面代码和图片:

图中显得了 Person 构造函数、Person 的原型属性以及 Person
现有的五个实例之间的涉及。在此,Person.prototype.constructor 指回了
PersonPerson.prototype 中除去含有 constructor
属性之外,还包括后来加上的其他属性。其余,要特别注意的是,即使这两个实例都不包含属性和章程,但大家却可以调用
person1.sayName()。这是因为里面指针 __proto__ 指向
Person.prototype,而在 Person.prototype 中能找到 sayName() 方法。

我们来证实一下,__proto__ 是不是的确指向 Person.prototype
的?如下代码所示:

function Person(){}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true

既然,__proto__ 确实是指向 Person.prototype,那么使用 new
操作符创设对象的经过可以衍生和变化为,为实例对象的 __proto__
赋值的长河。如下代码所示:

function Person(){}

// var person = new Person(); 
// 上一行代码等同于以下过程 ==> 
var person = {};
person.__proto__ = Person.prototype;
Person.call(person);

本条例子中,我先成立了一个空对象 person,然后把 person.__proto__
指向了 Person 的原型对象,便延续了 Person
原型对象中的所有属性和措施,最后又以 person 为功用域执行了 Person
函数,person 便就有着了 Person 的持有属性和章程。这一个历程和
var person = new Person(); 完全一样。

粗略来说,当大家走访一个对象的性能时,假设那一个特性不设有,那么就会去
__proto__ 里找,这个 __proto__ 又会有自己的
__proto__,于是就如此直白找下去,直到找到截止。在找不到的意况下,搜索过程接连要一环一环地前行到原型链末端才会停下来。

原型链

JavaScript
中描述了原型链的概念,并将原型链作为落实连续的显要方法。其基本思维是利用原型让一个引用类型继承另一个引用类型的习性和形式。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都富含一个针对性构造函数的指针,而实例都包含一个针对原型对象的中间指针。如下图所示:(图源:segmentfault.com,作者:manxisuo

这就是说,假诺我们让原型对象等于另一个体系的实例,结果会怎么着啊?显著,此时的原型对象将富含一个针对另一个原型的指针,相应地,另一个原型中也饱含着一个针对性另一个构造函数的指针。倘诺另一个原型又是另一个项目标实例,那么上述提到依旧创设,如此罕见递进,就重组了实例与原型的链子。那就是所谓原型链的基本概念。

地点这段话相比绕口,代码更易于了解,让我们来探望实现原型链的基本格局。如下代码所示:

function Father(){
    this.value = true;
}
Father.prototype.getValue = function(){
    return this.value;
};

function Son(){
    this.value2 = false;
}

// 继承了 Father
Son.prototype = new Father();

Son.prototype.getValue2 = function (){
    return this.value2;
};

var son = new Son();
console.log(son.getValue());  // true

以上代码定义了两个档次:Father
Son。每个门类分别有一个特性和一个措施。它们的要紧区别是 Son 继承了
Father,而连续是通过创设 Father 的实例,并将该实例赋给
Son.prototype
实现的。实现的面目是重写原型对象,代之以一个新品类的实例。换句话说,原来存在于
Father 的实例中的所有属性和办法,现在也设有于 Son.prototype
中了。在建立了继承关系随后,我们给 Son.prototype
添加了一个模式,这样就在持续了 Father
的习性和章程的基础上又添加了一个新章程。

我们再用 __proto__ 重写下边代码,更有利大家的明亮:

function Father(){
    this.value = true;
}
Father.prototype.getValue = function(){
    return this.value;
};

function Son(){
    this.value2 = false;
}

// 继承了 Father
// Son.prototype = new Father(); ==>
Son.prototype = {};
Son.prototype.__proto__ = Father.prototype;
Father.call(Son.prototype);

Son.prototype.getValue2 = function (){
    return this.value2;
};

// var son = new Son(); ==>
var son = {};
son.__proto__ = Son.prototype;
Son.call(son);

console.log(son.getValue()); // true
console.log(son.getValue === son.__proto__.__proto__.getValue); // true 

从上述代码可以见见,实例 son 调用 getValue() 方法,实际是透过了
son.__proto__.__proto__.getValue 的过程的,其中 son.__proto__ 等于
Son.prototype,而 Son.prototype.__proto__ 又等于
Father.prototype,所以 son.__proto__.__proto__.getValue 其实就是
Father.prototype.getValue

骨子里,前边例子中体现的原型链还少一环。我们知道,所有引用类型默然都连续了
Obeject,而这多少个连续也是因此原型链实现的。我们要铭记,所有函数的默认原型都是
Object 的实例,由此默认原型都会含有一个内部指针 __proto__,指向
Object.prototype。这也多亏拥有自定义类型都会继承
toString()valueOf() 等默认方法的根本原因。

下图突显了原型链实现连续的任何进程。(图源:segmentfault.com,作者:manxisuo

上图中,pprototype 属性,[p]__proto__
指对象的原型,[p]
形成的链(虚线部分)就是原型链。从图中可以得出以下新闻:

  • Object.prototype 是头等对象,所有目的都连续自它。
  • Object.prototype.__proto__ === null ,表达原型链到
    Object.prototype 终止。
  • Function.__proto__ 指向 Function.prototype

关卡

据悉描述写出相应的代码。

// 挑战一
// 1.定义一个构造函数 Animal,它有一个 name 属性,以及一个 eat() 原型方法。
// 2.eat() 的方法体为:console.log(this.name + " is eating something.")。
// 3.new 一个 Animal 的实例 tiger,然后调用 eat() 方法。
// 4.用 __proto__ 模拟 new Animal() 的过程,然后调用 eat() 方法。

var Animal = function(name){
    // 待补充的代码
};

var tiger = new Animal("tiger");
// 待补充的代码

var tiger2 = {};
// 待补充的代码

// 挑战二
// 1.定义一个构造函数 Bird,它继承自 Animal,它有一个 name 属性,以及一个 fly() 原型方法。
// 2.fly() 的方法体为:console.log(this.name + " want to fly higher.");。
// 3.new 一个 Bird 的实例 pigeon,然后调用 eat() 和 fly() 方法。
// 4.用 __proto__ 模拟 new Bird() 的过程,然后用代码解释 pigeon2 为何能调用 eat() 方法。

var Bird = function(name){
    // 待补充的代码
}

var pigeon = new Bird("pigeon");
// 待补充的代码

var pigeon2 = {};
// 待补充的代码

// 挑战三
// 1.定义一个构造函数 Swallow,它继承自 Bird,它有一个 name 属性,以及一个 nesting() 原型方法。
// 2.nesting() 的方法体为:console.log(this.name + " is nesting now.");。
// 3.new 一个 Swallow 的实例 yanzi,然后调用 eat()、fly() 和 nesting() 方法。
// 4.用 __proto__ 模拟 new Swallow() 的过程,然后用代码解释 yanzi2 为何能调用 eat() 方法。

var Swallow = function(name){
    // 待补充的代码
}

var yanzi = new Swallow("yanzi");
// 待补充的代码

var yanzi2 = {};
// 待补充的代码

更多

关怀微信公众号「劼哥舍」回复「答案」,获取关卡详解。
关注
https://github.com/stone0090/javascript-lessons,获取最新动态。