面向对象编程

面向对象编程是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程序开始执行时出现,这意味着不必明确实例化内置对象,它们已经被实例化了。

内置对象有两个,即GlobalMath,此外,它们也是本地对象,每个内置对象都是本地对象


3. 用instanceof来判断构造函数的实例

使用方法是:实例 instanceof 构造函数,结果会返回一个布尔值。

一般有两个常用的判断类型的方法:typeofinstanceof

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
    2
    function 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
2
3
4
5
6
function Mother(name) {
this.name = name;
this.father = "father";
}
let child1 = new Mother('c1');
let child2 = new Mother("c2");

这时发现其实child2的父亲并不是father,而是Father,我们修改child2father属性

1
2
child2.father = "Father";
console.log(child1.father); //father

当我们修改了child2father属性之后,child1father属性并没有改变,因为属性的值无法共享。

prototype属性所起的作用就是把需要共享的属性都放到构造函数的prototype上,这样每一个实例都能够获取到这些属性

接着需要知道构造函数和实例原型之间的关系:

上面提到,每个函数都有prototype属性,每个对象(null除外)在创建的时候就会有一个与之关联的对象,这个对象就是我们说的原型,每一个对象都会从原型“继承”属性。

构造函数的prototype属性

1
2
3
4
5
6
function Person() {}
Person.prototype.name = "nikkkki";
let person1 = new Person();
let person2 = new Person();
console.log(person1.name); //nikkkki
console.log(person2.name); //nikkkki

Person作为构造函数,在创建person1person2的时候,这两个实例都会与Personprototype属性关联,并从它继承属性,Person.prototype.nameprototype属性添加了name值,于是两个实例都能够获取到这个值。

所有的对象都是Object衍生出来的,因此所有的对象原型链终点都是Object.prototype的实例。

每个JavaScript对象(除了null)还有__proto__属性,它指向该对象的原型。

1
2
3
function Person() {}
let person = new Person();
console.log(Person.prototype === person.__proto__); //true

所以构造函数在构建实例时,除了有实例原型prototype,还让实例的原型__proto__指向了实力原型。

__proto__

前面5提到每个实例都有constructor属性,并且该属性指向这个实例的构造函数。

1
2
function Person() {}
console.log(Person === Person.prototype.constructor); //true

所以我们在这里加上constructor属性与prototype属性对应。

constructor

这里我们记住原型的3大定律

1
2
3
4
5
6
function Person() {}
let person = new Person();

console.log(person.__proto__ === Person.prototype) //true
console.log(Person.prototype.constructor === Person) //true
console.log(Object.getPrototypeOf(person) === Person.prototype) //true

那么原型链是什么呢?

所有对象的原型__proto__都指向构造函数的prototype属性,而所有的构造函数都是函数对象,所有的对象都是Object衍生出来的,也就是说所有的实例向上追溯__proto__,最终都会到达Object.prototype

1
2
3
4
5
function Person() {}
let person = new Person();
console.log(person.name); //undefined
Object.prototype.name = "nikkkki";
console.log(person.name); //nikkkki

我们可以将Object加入到关系图中:
Object作为源头

当查找实例的属性时,会现在实例本身查找,如果实例没有这个属性,就会在实例的构造函数的prototype上查找,一直到Object.prototype

**Object.prototype的原型是null**,所以查找到这一步就是终点了,如果还是查找失败,那就会返回null

原型链

这里我们可以来做一个小的逻辑运算题,想象一个函数对象Aprototype是另一个函数对象B构建出的实例,a实例就可以通过__proto__B的原型连接起来。

这里我们一定要记住原型3大定律的第一定律

1
2
3
function Father() {}
let son = new Father();
console.log(Father.prototype === son.__proto__); //true

在记住这一关系的情况下,AB的关系就很好理解了:

1
2
3
4
5
6
7
8
function A() {}
function B() {} // B.__proto__ === Object.prototype
let b = new B(); // b.__proto__ === B.prototype
A.prototype = b;
let a = new A(); // a.__proto__ === A.prototype === b
console.log(a.__proto__ === b); //true
console.log(a.__proto__.__proto__ === B.prototype) //true
console.log(a.__proto__.__proto__.__proto__ === Object.prototype) //true

总结一下:

  • 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
2
3
let obj = {};
obj.__proto__ = Father.prototype;
Father.call(obj);

new创建实例时经历了这样几步:

  1. 创建一个空对象obj
  2. 将空对象的__proto__指向构造函数的原型属性,也就是说obj原型属性上拥有了Father.prototype中的属性和方法
  3. 将构造函数中的this指针指向obj

Object.create()创建实例实现原理

1
2
3
4
5
Object.create = function(o) {
let F = function() {};
F.prototype = o;
return new F();
}

它在内部定义了一个对象,并且让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. Create a Basic JavaScript Object 创建JS对象
    1
    2
    3
    4
    let dog = {
    name: "Stone",
    numLegs: 4
    };
    1. Use Dot Notation to Access the Properties of an Object 获取对象的属性值
    1
    2
    3
    4
    5
    6
    7
    let dog = {
    name: "Spot",
    numLegs: 4
    };
    // Add your code below this line
    console.log(dog.name);
    console.log(dog.numLegs);
    1. Create a Method on an Object 为对象创建方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let 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
    7
    let dog = {
    name: "Spot",
    numLegs: 4,
    sayLegs: function() {return "This dog has " + this.numLegs + " legs.";}
    };

    dog.sayLegs();
    1. Define a Constructor Function 定义构造函数

    Constructors构造函数用来构造新的对象。对象的属性、行为都被定义在构造函数内。

    构造函数的写法规范有:

    • 构造函数的函数名要首字母大写
    • 构造函数this来指代将要创建的对象本身
    • 构造函数用来定义对象的属性和行为,而非像普通函数那样返回一个值
    1
    2
    3
    4
    5
    function Dog() {
    this.name = "Stone";
    this.color = "white";
    this.numLegs = 4;
    }
    1. Use a Constructor to Create Objects 利用构造函数创建对象

    new关键字调用构造函数。当调用构造函数时,JS会创建一个类的实例(instance)。如果没有使用new,那么构造函数内的this将不会指向新创建的对象。

    new关键字创建的实例将拥有类所有的属性,这些属性都可以被获取及修改

    1
    2
    3
    4
    5
    6
    7
    function Dog() {
    this.name = "Rupert";
    this.color = "brown";
    this.numLegs = 4;
    }
    // Add your code below this line
    let hound = new Dog();
    1. Extend Constructors to Receive Arguments 构造函数传参

    构造函数可以接受参数来创建不同的实例,这样使得构造函数更为灵活。

    1
    2
    3
    4
    5
    6
    7
    function Dog(name, color) {
    this.name = name;
    this.color = color;
    this.numLegs = 4;
    }

    let terrier = new Dog();
    1. Verify an Object’s Constructor with instanceof instanceof验证构造函数的实例

    构造函数创建的新的对象都叫做它的实例,检验一个对象是不是构造函数的实例可以使用instanceof方法,即实例 instanceof 构造函数,结果将返回一个布尔值。

    1
    2
    3
    4
    5
    6
    7
    function House(numBedrooms) {
    this.numBedrooms = numBedrooms;
    }

    // Add your code below this line
    let myHouse = new House(12);
    myHouse instanceof House;
    1. Understand Own Properties 自有属性

    如构造函数Bird(name)中的namenumLegs属性都叫函数的自有属性(own property),自有属性直接被定义给实例,即在构造函数中定义,这意味着构造函数Bird的实例都将各自拥有这些属性的副本。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function 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);
    }
    }
    1. Use Prototype Properties to Reduce Duplicate Code 原型属性

    构造函数的实例除了拥有自有属性,还拥有原型属性

    原型是所有实例共有的一个对象,这里形容它是“创建对象的‘菜谱’”。在JavaScript中,几乎所有对象都有原型,而且它们的原型是创建它们的构造函数的一部分,如下题中的beagle实例是由Dog构造函数创建的,beagle的原型就是Dog构造函数即Dog.prototype的一部分。

    1
    2
    3
    4
    5
    6
    function Dog(name) {
    this.name = name;
    }
    Dog.prototype.numLegs = 4;
    // Add your code above this line
    let beagle = new Dog("Snoopy");
    1. Iterate Over All Properties 遍历属性

    如上所说,对象有两种属性,一种是自有属性,是直接创建的实例本身就定义好的,即在构造函数中就定义了的,另一种是原型属性,是在原型上定义的。

    下面的例子中,在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而非undefinednumLegs这种&&直接添加在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
    19
    function 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);
    }
    }
    1. Understand the Constructor Property 实例的constructor属性

    实例都拥有一个特殊的属性constructor,这个属性指向创建这些实例的构造函数。如candidateDog创建的一个实例,candidate.constructor就是[function: Dog],但是constructor属性是可以被改写的,所以如果需要验证一个实例的构造函数时,不建议使用下题中的candidate.constructor === Dog这种方法,而是使用前面提到过的candidate instanceof Dog

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Dog(name) {
    this.name = name;
    }
    // Add your code below this line
    function joinDogFraternity(candidate) {
    if(candidate.constructor === Dog) {
    return true;
    }
    return false;
    }
    1. Change the Prototype to a New Object 让原型指向新的对象

    上面提到了构造函数有两种属性,自有属性和原型属性,我们可以通过添加原型属性来丰富构造函数的属性,但是如果需要添加的内容较多,代码就会看起来非常臃肿,这时我们可以采取为构造函数的原型指定一个新的对象的方法,在新的对象中包含所有我们需要添加给原型的属性或方法,这样代码会更简洁。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function 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();
    1. 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
    14
    function 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);
    }
    };
    1. Understand Where an Object’s Prototype Comes From 原型从何而来

    对象的原型(__proto__)都继承自它的构造函数的原型属性(prototype)。如beagleDog的实例,beagle的原型就是在let beagle = new Dog("snoopy")创建它的时候从Dog.prototype继承来的。isPrototypeOf方法可用用于验证对象的原型。Dog.prototype.isPrototypeOf(bealge)就是验证beagle的原型是不是从Dog.prototype继承而来。

    1
    2
    3
    4
    5
    6
    7
    8
    function Dog(name) {
    this.name = name;
    }

    let beagle = new Dog("Snoopy");

    // Add your code below this line
    Dog.prototype.isPrototypeOf(beagle);
    1. Understand the Prototype Chain 原型链

    几乎所有的JavaScript对象(除了null)都有原型prototype,这个prototype本身也是一个对象。因为对象的原型本身也是一个对象,也有它自己的原型prototype,如Dog.prototype的原型prototype就是Object.prototype

    如前面提到的检测对象自有属性的hasOwnProperty(property)方法就是定义在Object.prototype上的方法,Dog.prototype也继承到了这个方法,这种继承关系就是原型链。在这个原型链中,Dogbeagle的父类(**supertype),beagleDog的子类(subtype**),Object既是Dog的父类,也是beagle的父类。

    Object是JS中一切对象的父类,因此,任何对象都可以使用hasOwnProperty方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function 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);
    1. Use Inheritance So You Don’t Repeat Yourself DRY原则

    编程的一个原则叫做Don't Repeat Yourself(DRY),意思是不要重复写你写过的内容,因为重复的代码会很难维护,一旦要修改某个内容,就需要修改多处,这样会增加工作量,而且也更容易出错。

    这里举了一个例子,有BirdDog两个构造函数,它们都有describe()这个方法,内容是重复的,想要遵从DRY原则避免重复,一个方法是为这两个构造函数增加一个父类Animal,并为父类定义describe函数,这样BirdDog都能从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
    24
    function 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");
    }
    };
    1. Inherit Behaviors from a Supertype 继承

    像上一题中,我们先创建父类,然后让子类使用父类的方法的行为叫做继承。这一题的知识点在于如何创建父类的实例。通常我们会使用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
    16
    function 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"
    1. Set the Child’s Prototype to an Instance of the Parent 设置子类的原型

    创建实例之后,我们需要将子类的原型设置为父类原型的实例。如我们将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
    16
    function 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"
    1. Reset an Inherited Constructor Property 重设子类的constructor属性

    当一个对象继承另一个对象的原型prototype时,它也继承了父类的constructor属性,如Bird的原型继承了Animal的原型,这时Bird的实例如duckconstructor属性就会指向Animal而不是Bird。但我们仍需要duckconstructor指向Bird,这时就需要我们手动将Bird的原型的constructor属性Bird.prototype.constructor指向回Bird

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function 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();
    1. 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
    19
    function 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!"
    1. Override Inherited Methods 修改父类方法

    前面我们提到了,子类可以通过复制父类的原型来继承父类的方法,也可以通过给自己的原型绑定方法来添加自己的方法。同样的,子类也可以通过给自己的原型绑定父类的方法来修改父类的方法。

    当我们使用new关键词来创建实例,并且调用构造函数的方法时,如let duck = new Bird(); duck.eat(),JavaScript对duck's prototype机制是这样的:

    1. 先查看duck的原型属性是否有eat()方法,如果没有
    2. 查看Bird的原型属性是否有eat()方法,有,那么执行并停止搜索
    3. Animal也定义了eat()方法,但是因为JavaScript已经停止查找,所以无法达到这一层
    4. Object层,JavaScript已经停止查找,无法到达这一层
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function 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());
    1. Use a Mixin to Add Common Behavior Between Unrelated Objects Mixin实现多继承

    继承能够将一个对象的方法传递给另一个对象,但是有时两个并不相关的对象也会拥有一样的方法,比如BirdAirplane,这时无法用继承来共享方法,这时可以用mixins让不同的对象共享相同的方法。

    它的写法是:

    1. 创建一个mixin,这一题创建的是一个翱翔的glideMixin,它的参数是obj,即会使用这个mixin的对象
    2. mixin内创建obj的方法
    3. 调用mixin,将需要共享方法的对象以参数的形式传给mixin
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    let 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);
    1. Use Closure to Protect Properties Within an Object from Being Modified Externally 防止属性被更改

    在前面的习题中,实例的name属性是公开属性,即可以在定义bird实例的语句外部获取和修改属性。对于某些属性,如密码或者银行账户来说,这样实在太不安全了。

    最简单的创建私密属性的方法是在构造函数内部创建一个变量,这样能够将变量的作用域由全局转变为函数级作用域,变量只能在构造函数内部被获取和修改。

    1
    2
    3
    4
    5
    6
    function Bird() {
    let weight = 15;
    this.getWeight = function() {
    return weight;
    }
    }
    1. Understand the Immediately Invoked Function Expression (IIFE) 立即执行函数

    立即执行函数(IIFE)在JavaScript中是非常常见的一种形式,它的特征包括:

    • 没有函数名,不用变量存储

      函数被()包围,且函数末尾有一对括号()表明函数在声明之后会被立即执行

    1
    2
    3
    (function() {
    console.log("A cozy nest is ready");
    })()
    1. Use an IIFE to Create a Module

    立即执行函数常被用来将相关的功能组合成一个对象或模块。比如我们可以创建一个对象模块,让它是立即执行函数,在函数内部返回一个或多个mixin,这样我们在调用模块的时候可以同步调用mixin,如下题中,我们可以funModule.isCuteMixin(cat); cat.isCute,使代码更简洁。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let funModule = (function() {
    return {
    isCuteMixin: function(obj) {
    obj.isCute = function() {
    return true;
    };
    },
    singMixin: function(obj) {
    obj.sing = function() {
    console.log("Singing to an awesome tune");
    };
    }
    }
    })()