面向对象编程是JS里一个非常核心的思想,几乎所有面试的习题也都会涉及到这部分内容。这次FCC改版后的习题也单独把这部分内容做成了一个模块。以线性的创建父类和子类的顺序来讲解知识点。
1. 第一个核心概念是对象
在JavaScript中,万物皆对象。所谓对象,就是一种特殊的数据类型,拥有属性和方法。
- 第1题:创建JS对象,给对象添加属性
- 第2题:通过
obj.property
获取对象的属性值 - 第3题:为属性添加方法
- 第4题:在属性的方法中使用
this
关键字指代对象本身
2. 第二个知识点是构造函数constructor
构造函数用来创建对象,它的属性、方法都定义在函数内。
第5题:规范的构造函数写法(函数名首字母大写,this
指代对象、用于定义对象)
第6题:new
关键字创建构造函数的实例对象
第7题:构造函数可以接受参数
这里需要搞清楚几个概念的区别:
函数
1
2 >function foo() {...}
>var foo = function() {...}前者是函数声明,后者是函数表达式。两种写法的
typeof foo
结果都是function
函数对象
函数就是对象,代表函数的对象就是函数对象。在JavaScript的定义中,每一个函数实际上都是一个函数对象,JS代码中定义函数或者调用
Function
创建函数时,最终都以类似这样的形式调用Function
函数:var newFunc = new Function(funcArgs, funcBody)
。因此,在语法上,函数都称为函数对象。从用法上,如果我们单纯把它作为函数使用,那么它就是函数;如果我们通过它来实例化出对象来使用,那么它就可以当成一个函数对象来使用。在面向对象的范畴里面,函数对象类似于类的概念。
1
2
3
4
5
6 >let foo = new function() {...}
>typeof foo //object
>function Foo() {...}
>let foo = new Foo();
>typeof foo //object弄清楚函数和对象的概念能让我们更好地理解
__proto__
和prototype
还有两个对象的概念也需要我们了解:
本地对象
本都对象(native object)被定义为独立于宿主环境的ECMAScript实现提供的对象,简单来说,就是ECMA-262定义的类(引用类型),包括
Object, Function, Array, String, Boolean, Number, Date, RegExp, Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError
。
1
2
3
4 >typeof(Object);
>typeof(Array);
>typeof(String);
>...以上返回结果都是
function
,即以上本地对象都是通过function
建立起来的(比较容易理解的方法是这些对象在创建的过程中,都是可以通过类似let str = new String()
这样的方法来创建的)。
1 >function Object() {...}可以看出
Object
原本就是一个函数,通过new Object()
实例化后创建对象。内置对象
内置对象(built-in object)即由ECMAScript实现提供的、独立于宿主环境的所有对象,在ECMAScript程序开始执行时出现,这意味着不必明确实例化内置对象,它们已经被实例化了。
内置对象有两个,即
Global
和Math
,此外,它们也是本地对象,每个内置对象都是本地对象
3. 用instanceof
来判断构造函数的实例
使用方法是:实例 instanceof 构造函数
,结果会返回一个布尔值。
一般有两个常用的判断类型的方法:
typeof
和instanceof
typeof
用于判断变量的类型我们可以用
typeof
来判断number, string, object, boolean, function, undefined, symbol
这七种类型。缺陷:
在判断不是
object
类型的数据的时候,typeof
能够清楚地告诉我们具体是哪一类的数据,但是在判断
object
数据时,只能告诉我们这个数据是object
1
2
3
4 let str1 = "hello";
let str2 = new String("hello");
typeof str1; //string
typeof str2; //object需要判断数据是具体哪一种
object
的时候,我们需要利用instanceof
这个操作符。
typeof
的实现原理JavaScript在底层存储变量时,不同对象在底层都表现为二进制,且会在变量的机器码的低位1-3位存储其类型信息,如:
- 000: 对象
- 010: 浮点数
- 100: 字符串
- 110: 布尔值
- 1: 整数
根据JS判断类型的代码,
typeof
会
- 先判断是否为
undefined
- 如果不是
undefined
,判断是否为对象- 如果不是对象,判断是否为数字
- …
所以这里有一个bug,
null
的所有机器码均为0,如果用typeof
来判断,会被判断为object
。但用instanceof
来判断又会出现矛盾的结果。
1
2
3 typeof null; //"object"
null instanceof Object //false
null instanceof null //TypeError: Right-hand side of 'instanceof' is not an object所以在使用
typeof
判断变量的时候,要注意,尽量使用typeof
来判断基础数据类型,避免对null
的判断。替换方案
我们可以利用
Object.prototype.toString.call()
方法来实现对变量类型的比较准确的判断
1
2
3
4
5
6
7
8
9 Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('hi') // "[object String]"
Object.prototype.toString.call({a:'hi'}) // "[object Object]"
Object.prototype.toString.call([1,'a']) // "[object Array]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(()=>{}) // "[object Function]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
instanceof
用于判断对象的具体类型
instanceof
主要用于判断一个实例是否属于某个类型。如
1
2
3
4
5 let Person = function() {
}
let ps1 = new Person();
ps1 instanceof Person //true
instanceof
也可以判断一个实例是否是其父类或者祖父类的实例。
1
2
3
4
5
6 let Person = function() {}
let Programmer = function() {}
Programmer.prototype = new Person();
let ps1 = new Programmer();
ps1 instanceof Person //true
ps1 instanceof Programmer //true
instanceof
实现原理这里引用浅谈 instanceof 和 typeof 的实现原理里的一段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13 function new_instance_of(leftVaule, rightVaule){
let rightProto = rightVaule.prototype;// 取右表达式的 prototype 值
leftVaule = leftVaule.__proto__;// 取左表达式的__proto__值
while(true){
if(leftVaule ===null){
return false;
}
if(leftVaule === rightProto){
return true;
}
leftVaule = leftVaule.__proto__
}
}
instanceof
的实现原理就是只要右边变量的prototype
在左边变量的原型链上即可,上面这段代码会遍历左边变量的原型链,直到找到右边变量的prototype
,如果没有找到,则返回false
.具体原型链如何实现继承,我们在后面会讲到。
4. 对象的自有属性和原型属性
- 构造函数中定义的属性叫做自有属性,自有属性直接被定义给实例,即实例各自拥有这些属性的副本
- 添加在原型
prototype
上的属性叫做原型属性,原型是实例共有的对象,添加在原型上的属性也能通过实例获取到
通过**for(let property in object)
可以遍历对象的属性,通过hasOwnProperty(property)
**方法可用区分自有属性和原型属性
5. 实例的**constructor
**属性
实例都拥有一个特殊的属性constructor
,指向创建这个实例的构造函数。
但是实例的constructor
属性可以被改写,所以object.constructor
来获得的结果不一定是object
的构造函数,需要验证实例的构造函数时,还是使用instanceof
6. 修改构造函数原型
当需要添加的原型属性太多时,我们可以直接给构造函数指定新的原型,在新的原型的对象中添加需要给原型的属性和方法。
需要注意的是,这个过程中会抹去构造函数的constructor
属性,所以需要我们手动地添加定义constructor
的语句(可以在新的prototype
对象中定义)
7. 原型和原型链
原型和原型链是面向对象编程部分的重点和难点。
首先需要辨析两个关于原型概念:
prototype
属性:前面原型属性部分就提到了prototype
,**prototype
是每个函数都有的属性**,但不是每个对象都有的属性1
2function Foo() {...} //Foo.prototype: Foo {}
let foo = new Foo() //foo.prototype: undefined, foo.__proto__: Foo {}__proto__
:__proto__
是每个函数和对象都隐含的一个属性,它指向创建它的构造函数的prototype
,即**Supertype.prototype === subtype.__proto__
**。
其次,我们需要知道为什么存在prototype
属性
JavaScript里所有的数据类型都是对象,为了实现面向对象的思想,就必须实现继承来把所有的对象都连接起来。JavaScript是通过new
来创建实例的,比如我们创建两个实例:
1 | function Mother(name) { |
这时发现其实child2
的父亲并不是father
,而是Father
,我们修改child2
的father
属性
1 | child2.father = "Father"; |
当我们修改了child2
的father
属性之后,child1
的father
属性并没有改变,因为属性的值无法共享。
而prototype
属性所起的作用就是把需要共享的属性都放到构造函数的prototype
上,这样每一个实例都能够获取到这些属性。
接着需要知道构造函数和实例原型之间的关系:
上面提到,每个函数都有prototype
属性,每个对象(null
除外)在创建的时候就会有一个与之关联的对象,这个对象就是我们说的原型,每一个对象都会从原型“继承”属性。
1 | function Person() {} |
Person
作为构造函数,在创建person1
和person2
的时候,这两个实例都会与Person
的prototype
属性关联,并从它继承属性,Person.prototype.name
给prototype
属性添加了name
值,于是两个实例都能够获取到这个值。
所有的对象都是Object
衍生出来的,因此所有的对象原型链终点都是Object.prototype
的实例。
每个JavaScript对象(除了null
)还有__proto__
属性,它指向该对象的原型。
1 | function Person() {} |
所以构造函数在构建实例时,除了有实例原型prototype
,还让实例的原型__proto__
指向了实力原型。
前面5
提到每个实例都有constructor
属性,并且该属性指向这个实例的构造函数。
1 | function Person() {} |
所以我们在这里加上constructor
属性与prototype
属性对应。
这里我们记住原型的3大定律:
1 | function Person() {} |
那么原型链是什么呢?
所有对象的原型__proto__
都指向构造函数的prototype
属性,而所有的构造函数都是函数对象,所有的对象都是Object
衍生出来的,也就是说所有的实例向上追溯__proto__
,最终都会到达Object.prototype
1 | function Person() {} |
我们可以将Object
加入到关系图中:
当查找实例的属性时,会现在实例本身查找,如果实例没有这个属性,就会在实例的构造函数的prototype
上查找,一直到Object.prototype
。
**Object.prototype
的原型是null
**,所以查找到这一步就是终点了,如果还是查找失败,那就会返回null
这里我们可以来做一个小的逻辑运算题,想象一个函数对象A
的prototype
是另一个函数对象B
构建出的实例,a
实例就可以通过__proto__
和B
的原型连接起来。
这里我们一定要记住原型3大定律的第一定律
1 | function Father() {} |
在记住这一关系的情况下,A
和B
的关系就很好理解了:
1 | function A() {} |
总结一下:
- JS中所有东西都是对象,函数也是对象,是一种特殊的对象
- JS中所有东西都是由
Object
衍生而来,所有对象原型链的重点都指向Object.prototype
- 对象都有一个隐含的
__proto__
属性,它指向创建它的构造函数的原型,Object.prototype.__proto__
指向null
- 实例的
__proto__
指向创建它的构造函数的prototype
,构造函数的__proto__
也指向它的构造函数的prototype
,一级一级向上,知道Object.prototype
8. Object.create
创建实例
####继承
基于上述原型链的关系,子类可以继承父类的属性和方法。
但是继承是一种比较有迷惑性的说法,引用《你不知道的JavaScript》中的话:
继承意味着复制操作,然而JavaScript默认并不会复制对象的属性,相反,JavaScript只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。
这里需要注意的地方是,我们在创建实例时,常使用new
关键字,这里建议使用Object.create
来创建实例,那么这两者的区别是什么呢?
new()
创建实例实现原理
1 | let obj = {}; |
new
创建实例时经历了这样几步:
- 创建一个空对象
obj
- 将空对象的
__proto__
指向构造函数的原型属性,也就是说obj
原型属性上拥有了Father.prototype
中的属性和方法 - 将构造函数中的
this
指针指向obj
Object.create()
创建实例实现原理
1 | Object.create = function(o) { |
它在内部定义了一个对象,并且让F.prototype
对象赋值为引进的对象/函数o
,然后返回新对象。
DRY原则
正是因为可以从原型链上继承也好,委托也好,父类的属性和方法,因此我们应该多利用这一的特性,将子类 上重复的方法尽量放到父类上,这一可以保持代码的简洁,同时增强可维护性。
继承父类的属性
当我们先创建父类,并制定父类的prototype
后,如果再来创建实例,使用new
会产生一些问题,所以建议采用Object.create(supertype.prototype)
的方法。
创建后,我们需要将子类的原型设置为父类原型的实例,即Son.prototype = Object.create(Father.prototype)
,这样子类的原型就继承了父类原型的所有方法和属性。
但此时继承父类的原型会造成子类同时继承父类的constructor
,所以还需要手动地将子类的Son.prototype.constructor = Son
。
自己的方法
在继承父类的属性方法后,子类也可以自己添加方法,或者修改父类已有的方法。方法都是直接在子类的原型上修改。
###9. Mixin
多继承
用于实现两个不相关的对象共享方法
10. 私有属性
防止构造函数内部属性被修改,可以在构造函数内部创建变量来保存属性值。
11. 立即执行函数
立即执行函数的特征是即写即用。
利用立即执行函数可以把相关功能组合成一个模块,模块返回一个对象,对象中可以包含Mixin
,这样在调用模块的时候就可以同步调用Mixin
.
以下是这部分习题的解答,每题的知识点我都有些,有些知识点可能和上面的有些重复.
习题Introduction to the Object Oriented Programming Challenges
-
1
2
3
4let dog = {
name: "Stone",
numLegs: 4
}; -
1
2
3
4
5
6
7let dog = {
name: "Spot",
numLegs: 4
};
// Add your code below this line
console.log(dog.name);
console.log(dog.numLegs); - Create a Method on an Object 为对象创建方法
1
2
3
4
5
6
7
8
9let dog = {
name: "Spot",
numLegs: 4,
sayLegs: function() {
return "This dog has 4 legs."
}
};
dog.sayLegs();Make Code More Reusable with the this Keyword
this
关键字1
2
3
4
5
6
7let dog = {
name: "Spot",
numLegs: 4,
sayLegs: function() {return "This dog has " + this.numLegs + " legs.";}
};
dog.sayLegs();-
Constructors
构造函数用来构造新的对象。对象的属性、行为都被定义在构造函数内。构造函数的写法规范有:
- 构造函数的函数名要首字母大写
- 构造函数用
this
来指代将要创建的对象本身 - 构造函数用来定义对象的属性和行为,而非像普通函数那样返回一个值
1
2
3
4
5function Dog() {
this.name = "Stone";
this.color = "white";
this.numLegs = 4;
} - Use a Constructor to Create Objects 利用构造函数创建对象
用
new
关键字调用构造函数。当调用构造函数时,JS会创建一个类的实例(instance)。如果没有使用new
,那么构造函数内的this
将不会指向新创建的对象。用
new
关键字创建的实例将拥有类所有的属性,这些属性都可以被获取及修改1
2
3
4
5
6
7function Dog() {
this.name = "Rupert";
this.color = "brown";
this.numLegs = 4;
}
// Add your code below this line
let hound = new Dog();-
构造函数可以接受参数来创建不同的实例,这样使得构造函数更为灵活。
1
2
3
4
5
6
7function Dog(name, color) {
this.name = name;
this.color = color;
this.numLegs = 4;
}
let terrier = new Dog(); - Verify an Object’s Constructor with instanceof
instanceof
验证构造函数的实例
构造函数创建的新的对象都叫做它的实例,检验一个对象是不是构造函数的实例可以使用
instanceof
方法,即实例 instanceof 构造函数
,结果将返回一个布尔值。1
2
3
4
5
6
7function House(numBedrooms) {
this.numBedrooms = numBedrooms;
}
// Add your code below this line
let myHouse = new House(12);
myHouse instanceof House;- Verify an Object’s Constructor with instanceof
-
如构造函数
Bird(name)
中的name
和numLegs
属性都叫函数的自有属性(own property),自有属性直接被定义给实例,即在构造函数中定义,这意味着构造函数Bird
的实例都将各自拥有这些属性的副本。1
2
3
4
5
6
7
8
9
10
11
12function Bird(name) {
this.name = name;
this.numLegs = 2;
}
let canary = new Bird("Tweety");
let ownProps = [];
// Add your code below this line
for(let property in canary) {
if(canary.hasOwnProperty(property)) {
ownProps.push(property);
}
} -
构造函数的实例除了拥有自有属性,还拥有原型属性。
原型是所有实例共有的一个对象,这里形容它是“创建对象的‘菜谱’”。在JavaScript中,几乎所有对象都有原型,而且它们的原型是创建它们的构造函数的一部分,如下题中的
beagle
实例是由Dog
构造函数创建的,beagle
的原型就是Dog
构造函数即Dog.prototype
的一部分。1
2
3
4
5
6function Dog(name) {
this.name = name;
}
Dog.prototype.numLegs = 4;
// Add your code above this line
let beagle = new Dog("Snoopy"); -
如上所说,对象有两种属性,一种是自有属性,是直接创建的实例本身就定义好的,即在构造函数中就定义了的,另一种是原型属性,是在原型上定义的。
下面的例子中,在
Dog
构造函数中就定义了name
属性,所以所有的Dog
的实例,如beagle
都在创建时就拥有name
属性,在let beagle = new Dog("snoopy");
时,beagle
就有了name
属性及赋予了自己的name
属性的值snoopy
,所以**name
就是所有Dog
实例的自有属性。此外,Dog
还在自己的原型上添加了numLegs
属性,即Dog.prototype.numLegs = 4;
,实例同样拥有在原型上添加的属性,所以beagle.numLegs
可以获得4
而非undefined
。numLegs
这种&&直接添加在Dog.prototype
上的属性就是原型属性。实例同时拥有自有属性和原型属性,但是可以通过
instance.hasOwnProperty(property)
来判断是哪一类属性。这一题就通过这个方法将beagle
的不同属性分别添加到ownProps
自有属性数组和prototypeProps
原型数组中。如果输出两个数组,得到的结果是ownProps: ['name'] prototypeProps: ['numLegs']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function Dog(name) {
this.name = name;
}
Dog.prototype.numLegs = 4;
let beagle = new Dog("Snoopy");
let ownProps = [];
let prototypeProps = [];
// Add your code below this line
for(let property in beagle) {
if(beagle.hasOwnProperty(property)) {
ownProps.push(property);
} else {
prototypeProps.push(property);
}
} - Understand the Constructor Property 实例的
constructor
属性
实例都拥有一个特殊的属性
constructor
,这个属性指向创建这些实例的构造函数。如candidate
是Dog
创建的一个实例,candidate.constructor
就是[function: Dog]
,但是constructor
属性是可以被改写的,所以如果需要验证一个实例的构造函数时,不建议使用下题中的candidate.constructor === Dog
这种方法,而是使用前面提到过的candidate instanceof Dog
。1
2
3
4
5
6
7
8
9
10function Dog(name) {
this.name = name;
}
// Add your code below this line
function joinDogFraternity(candidate) {
if(candidate.constructor === Dog) {
return true;
}
return false;
}- Understand the Constructor Property 实例的
- Change the Prototype to a New Object 让原型指向新的对象
上面提到了构造函数有两种属性,自有属性和原型属性,我们可以通过添加原型属性来丰富构造函数的属性,但是如果需要添加的内容较多,代码就会看起来非常臃肿,这时我们可以采取为构造函数的原型指定一个新的对象的方法,在新的对象中包含所有我们需要添加给原型的属性或方法,这样代码会更简洁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14function Dog(name) {
this.name = name;
}
Dog.prototype = {
// Add your code below this line
numLegs: 4,
eat: function() {
console.log("yummy");
},
describe: function() {
console.log("My name is " + this.name);
}
};
let dog = new Dog();- Remember to Set the Constructor Property when Changing the Prototype 在新对象内设置
constructor
属性
当我们人为地将原型指向一个新的对象时,有一个副作用就是这样会抹去构造函数的
constructor
属性。这时我们需要手动地添加上定义constructor
属性的语句。如下题,在给Dog
指定的新原型中加入了constructor: Dog
来明确新原型的constructor
属性还是Dog
.1
2
3
4
5
6
7
8
9
10
11
12
13
14function Dog(name) {
this.name = name;
}
// Modify the code below this line
Dog.prototype = {
constructor: Dog,
numLegs: 2,
eat: function() {
console.log("nom nom nom");
},
describe: function() {
console.log("My name is " + this.name);
}
};- Remember to Set the Constructor Property when Changing the Prototype 在新对象内设置
-
对象的原型(
__proto__
)都继承自它的构造函数的原型属性(prototype
)。如beagle
是Dog
的实例,beagle
的原型就是在let beagle = new Dog("snoopy")
创建它的时候从Dog.prototype
继承来的。isPrototypeOf
方法可用用于验证对象的原型。Dog.prototype.isPrototypeOf(bealge)
就是验证beagle
的原型是不是从Dog.prototype
继承而来。1
2
3
4
5
6
7
8function Dog(name) {
this.name = name;
}
let beagle = new Dog("Snoopy");
// Add your code below this line
Dog.prototype.isPrototypeOf(beagle); -
几乎所有的JavaScript对象(除了null)都有原型
prototype
,这个prototype
本身也是一个对象。因为对象的原型本身也是一个对象,也有它自己的原型prototype
,如Dog.prototype
的原型prototype
就是Object.prototype
。如前面提到的检测对象自有属性的
hasOwnProperty(property)
方法就是定义在Object.prototype
上的方法,Dog.prototype
也继承到了这个方法,这种继承关系就是原型链。在这个原型链中,Dog
是beagle
的父类(**supertype
),beagle
是Dog
的子类(subtype
**),Object
既是Dog
的父类,也是beagle
的父类。Object
是JS中一切对象的父类,因此,任何对象都可以使用hasOwnProperty
方法。1
2
3
4
5
6
7
8
9
10function Dog(name) {
this.name = name;
}
let beagle = new Dog("Snoopy");
Dog.prototype.isPrototypeOf(beagle); // => true
// Fix the code below so that it evaluates to true
Object.prototype.isPrototypeOf(Dog.prototype); -
编程的一个原则叫做
Don't Repeat Yourself(DRY)
,意思是不要重复写你写过的内容,因为重复的代码会很难维护,一旦要修改某个内容,就需要修改多处,这样会增加工作量,而且也更容易出错。这里举了一个例子,有
Bird
和Dog
两个构造函数,它们都有describe()
这个方法,内容是重复的,想要遵从DRY
原则避免重复,一个方法是为这两个构造函数增加一个父类Animal
,并为父类定义describe
函数,这样Bird
和Dog
都能从Animal
继承describe
方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24function Cat(name) {
this.name = name;
}
Cat.prototype = {
constructor: Animal,
};
function Bear(name) {
this.name = name;
}
Bear.prototype = {
constructor: Animal,
};
function Animal() { }
Animal.prototype = {
constructor: Animal,
eat: function() {
console.log("nom nom nom");
}
}; -
像上一题中,我们先创建父类,然后让子类使用父类的方法的行为叫做继承。这一题的知识点在于如何创建父类的实例。通常我们会使用
new
关键字来创建实例,但是这样在继承父类的属性和方法时会有一些问题,所以更建议使用Object.create(supertype.prototype)
。Object.create(obj)
方法会创建一个新的对象,然后将参数obj
作为新创建的对象的原型prototype
,这样就能让新创建的对象实例拥有原型的方法和属性。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function Animal() { }
Animal.prototype = {
constructor: Animal,
eat: function() {
console.log("nom nom nom");
}
};
// Add your code below this line
let duck = Object.create(Animal.prototype); // Change this line
let beagle = Object.create(Animal.prototype); // Change this line
duck.eat(); // Should print "nom nom nom"
beagle.eat(); // Should print "nom nom nom" -
创建实例之后,我们需要将子类的原型设置为父类原型的实例。如我们将
Dog
的原型设置为Animal
的原型的实例:Dog.prototype = Object.create(Animal.prototype)
,这样Dog
的原型就继承了Animal
原型的所有方法和属性,Dog
的实例如beagle
就能继承Animal
的方法如eat
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function Animal() { }
Animal.prototype = {
constructor: Animal,
eat: function() {
console.log("nom nom nom");
}
};
function Dog() { }
// Add your code below this line
Dog.prototype = Object.create(Animal.prototype);
let beagle = new Dog();
beagle.eat(); // Should print "nom nom nom" - Reset an Inherited Constructor Property 重设子类的
constructor
属性
当一个对象继承另一个对象的原型
prototype
时,它也继承了父类的constructor
属性,如Bird
的原型继承了Animal
的原型,这时Bird
的实例如duck
的constructor
属性就会指向Animal
而不是Bird
。但我们仍需要duck
的constructor
指向Bird
,这时就需要我们手动将Bird
的原型的constructor
属性Bird.prototype.constructor
指向回Bird
。1
2
3
4
5
6
7
8
9
10
11
12
13function Animal() { }
function Bird() { }
function Dog() { }
Bird.prototype = Object.create(Animal.prototype);
Dog.prototype = Object.create(Animal.prototype);
// Add your code below this line
Bird.prototype.constructor = Bird;
Dog.prototype.constructor = Dog;
let duck = new Bird();
let beagle = new Dog();- Reset an Inherited Constructor Property 重设子类的
- Add Methods After Inheritance 构造函数添加方法
当构造函数继承父类的原型时,除了拥有父类的方法,还可以添加自己的方法。
通过添加方法到子类的原型上,我们可以让子类拥有自己独特的方法,如
Bird
继承了Animal
的方法,但通过Bird.prototype.fly = function() {...}
可以给Bird
添加fly()
方法。此时,
Bird
的实例就拥有了eat()
和fly()
两个方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19function Animal() { }
Animal.prototype.eat = function() { console.log("nom nom nom"); };
function Dog() { }
// Add your code below this line
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log("Woof");
}
// Add your code above this line
let beagle = new Dog();
beagle.eat(); // Should print "nom nom nom"
beagle.bark(); // Should print "Woof!"- Override Inherited Methods 修改父类方法
前面我们提到了,子类可以通过复制父类的原型来继承父类的方法,也可以通过给自己的原型绑定方法来添加自己的方法。同样的,子类也可以通过给自己的原型绑定父类的方法来修改父类的方法。
当我们使用
new
关键词来创建实例,并且调用构造函数的方法时,如let duck = new Bird(); duck.eat()
,JavaScript对duck's prototype
机制是这样的:- 先查看
duck
的原型属性是否有eat()
方法,如果没有 - 查看
Bird
的原型属性是否有eat()
方法,有,那么执行并停止搜索 Animal
也定义了eat()
方法,但是因为JavaScript已经停止查找,所以无法达到这一层Object
层,JavaScript已经停止查找,无法到达这一层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function Bird() { }
Bird.prototype.fly = function() { return "I am flying!"; };
function Penguin() { }
Penguin.prototype = Object.create(Bird.prototype);
Penguin.prototype.constructor = Penguin;
// Add your code below this line
Penguin.prototype.fly = function() { return "Alas, this is a flightless bird."}
// Add your code above this line
let penguin = new Penguin();
console.log(penguin.fly());-
继承能够将一个对象的方法传递给另一个对象,但是有时两个并不相关的对象也会拥有一样的方法,比如
Bird
和Airplane
,这时无法用继承来共享方法,这时可以用mixins
让不同的对象共享相同的方法。它的写法是:
- 创建一个
mixin
,这一题创建的是一个翱翔的glideMixin
,它的参数是obj
,即会使用这个mixin
的对象 - 在
mixin
内创建obj
的方法 - 调用
mixin
,将需要共享方法的对象以参数的形式传给mixin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let bird = {
name: "Donald",
numLegs: 2
};
let boat = {
name: "Warrior",
type: "race-boat"
};
// Add your code below this line
let glideMixin = function(obj) {
obj.glide = function() {
console.log("glide");
}
}
glideMixin(bird);
glideMixin(boat); - 创建一个
-
在前面的习题中,实例的
name
属性是公开属性,即可以在定义bird
实例的语句外部获取和修改属性。对于某些属性,如密码或者银行账户来说,这样实在太不安全了。最简单的创建私密属性的方法是在构造函数内部创建一个变量,这样能够将变量的作用域由全局转变为函数级作用域,变量只能在构造函数内部被获取和修改。
1
2
3
4
5
6function Bird() {
let weight = 15;
this.getWeight = function() {
return weight;
}
} -
立即执行函数(IIFE)在JavaScript中是非常常见的一种形式,它的特征包括:
没有函数名,不用变量存储
函数被
()
包围,且函数末尾有一对括号()
表明函数在声明之后会被立即执行
1
2
3(function() {
console.log("A cozy nest is ready");
})() -
立即执行函数常被用来将相关的功能组合成一个对象或模块。比如我们可以创建一个对象模块,让它是立即执行函数,在函数内部返回一个或多个
mixin
,这样我们在调用模块的时候可以同步调用mixin
,如下题中,我们可以funModule.isCuteMixin(cat); cat.isCute
,使代码更简洁。1
2
3
4
5
6
7
8
9
10
11
12
13
14let funModule = (function() {
return {
isCuteMixin: function(obj) {
obj.isCute = function() {
return true;
};
},
singMixin: function(obj) {
obj.sing = function() {
console.log("Singing to an awesome tune");
};
}
}
})()