初步了解原型链与继承

原型相关的内容是 JavaScript 中的重要概念, 它相似于经典面向对象语言中的类, 但又不彻底相同, 原型的主要做用是实现对象的继承。这篇文章将讨论 JavaScript 中的原型、原型链、利用原型链完成继承、ES6中的 class 语法糖在内的几个内容.函数

1. 原型和原型链

1. 原型和原型链

JavaScript 中每一个对象都有原型的引用, 当查找一个对象的属性时, 若是在对象自己上找不到, 就会去他所连接到的原型上查找, 它连接到的原型上也会连接到其余的原型, 这样就造成了所谓的原型链, 沿着原型链一路寻找下去, 若是找到头仍是没有这个属性, 则认为这个对象上不存在此属性。ui

例以下面的代码中建立了3个对象, 每一个对象都有本身特有的属性, 并且在每一个对象上找不到其余对象的属性:this

// demo. 1 - in
let obj1 = { prop1: 1 };
let obj2 = { prop2: 2 };
let obj3 = { prop3: 3 };

// // 判断对象是否是有各自的属性
console.log(obj1.prop1, obj2.prop2, obj3.prop3);  // 1 2 3

// 判断对象是否是有其余对象的属性
console.log(obj1.prop2, obj1.prop3); // undefined undefined
复制代码

上面的代码能够看出一个对象上只有本身定义的属性, 没有其余对象的属性。spa

经过内置的方法 Object.setPrototypeOf(A, B) 能够把对象 A 的原型设成对象 B, 也就是说对象 B 成了对象 A 的原型。 此后若是要查找对象 A 的某个属性时, 若是在A上找不到, 就会去对象B中寻找。prototype

例以下面的代码经过 Object.setPrototypeOf 方法将 对象2 设成 对象1 的原型, 将 对象3 设成 对象2 的原型, 而后去访问 对象1 和 对象2 原本没有的属性:3d

// demo.2 - setprototypeof
let obj1 = { prop1: 1 };
let obj2 = { prop2: 2 };
let obj3 = { prop3: 3 };

// 将 对象2 设成 对象1 的原型, 将 对象3 设成 对象2 的原型
Object.setPrototypeOf(obj1, obj2); 
Object.setPrototypeOf(obj2, obj3);

// 如今检查 对象1 上面是否有属性 prop2 和 prop3
console.log(obj1.prop2, obj1.prop3); // 2 3

// 检查 对象2 上是否是有属性 prop3
console.log(obj2.prop3); // 3
复制代码

经过上面的代码发现将一个对象设为原型后, 就能够访问这个原型对象上的属性了。上面的这段代码, 将 obj2 设置成了 obj1 的原型, 将 obj3 设置成了 obj2 的原型, 这样就产生了一条原型链: obj1 -> obj2 -> obj3。经过图片能够更加具象的理解这个问题: code

从图中能够看到,Obj3的原型属性仍然指向了其余方向,这说明Obj3也是有原型的,不只如此,除非手动的将一个对象的原型指向null,不然除了一个对象以外,全部对象都有本身的原型。那么这个没有原型的对象是谁呢?这个问题的答案和下面的问题的答案同样:就是Object.prototype,也就是Object对象的原型对象。这要结合下面的问题来讲明。cdn

那么,上面图中查找的过程有没有终点呢?答案是确定的,终点是上面提到的 Object对象的原型对象,也就是Object.prototype,这是属性查找的终点, 若是在Object.prototype仍找不到但愿的属性,则对象就被认为不拥有这个属性。对象

这说明了一个重要的问题:全部对象的原型最终都会连接到Object.prototype,不管中间通过多少其余的原型对象。blog

从前有句话叫: 顺着网线去打你; 如今能够叫: 顺着原型链去找你。

1.2 当要找的属性存在于自身时

上面说, 当查找一个对象的属性时, 会在对象上查找, 若是找不到就顺着原型连接着找, 知道找到或者原型链到头了为止。

也就是说当能在本身身上找到想要的属性时,就不用去原型链上寻找了。例以下面的例子:

let obj1 = { 
    prop1: 1,
    prop2: '呵呵呵' // 增长一个名为 prop2 的属性并将值设置成字符创类型
};

// 注意 obj2 的 prop2 属性值是 2, 数值类型
let obj2 = { 
    prop2: 2  
};

// 将 对象2 设成 对象1 的原型
Object.setPrototypeOf(obj1, obj2); 

// 这个时候 obj2 的 prop2 属性已经在原型链上了
// 实验看 obj1.prop2 属性究竟是哪一个
console.log(obj1.prop2);  // '呵呵呵'
复制代码

上面的代码将 obj2 设置成 obj1 的原型, 当 obj1 想找 prop2 属性时, 在自身就找到了, 因此不用去它的原型 obj2 中寻找了。这个实验说明了在寻找属性时会从对象自身开始, 向着原型的方向去找, 找到的第一个就拿来直接用。

这种行为也是利用原型链实现继承的基础之一。

2. 构造函数的原型

2.1 实例的原型指向的是构造函数的原型

同过上面的部分已经知道对象有本身的原型, 而函数也是对象, 因此函数也有原型, 一样的, 构造函数也有原型。

构造函数的用法是配合 new 操做符建立一个类的实例,建立出的实例也是对象, 因此也是有原型的, 并且实例的原型就指向构造函数的原型。

还有一点是, 构造函数的原型中有个属性名为 constructor, 这是个引用类型的属性, 它指向构造函数自己。

利用实例的原型指向构造函数的原型能够实现一种巧妙的继承方式: 将要建立的实例的不共享的属性放在构造函数内部, 而公用的属性定义在构造函数的原型上, 这样建立出的实例即有各自不一样的属性, 还能顺着原型链找到公用的属性。如下面的代码为例:

// 1. 定义一个构造函数 Person
function Person(name){
    this.name = name;
}
Person.prototype.sayName = function(){
    console.log('个人名字是: ', this.name);
}

// 2. 利用构造函数实例化两个 Person 的实例
let xm = new Person('小明');
let xh = new Person('小红');

// 3. 实例调用公有的函数来输出私有的属性
xm.sayName(); // 个人名字是: 小明
xh.sayName(); // 个人名字是: 小红
复制代码

上面代码中定义的 Person 构造函数有两个属性: namesayName, 并且分别将这两个属性定义在了构造函数体内和构造函数的原型上, 这样经过构造函数 new 出来的实例拥有各自的私有属性 name, 还可使用公有的属性 sayName 函数来访问本身的私有属性, 经过代码的运行结果能够看出这种方法的有效性。

能够将上面的例子中构造函数、实例与原型之间的关系用图表示出来:

将 sayName 定义在构造函数的原型中的好处是,产生的每一个实例都会共享这个 sayName, 若是在构造函数自身上定义 sayName 的话, 每一个实例就都会有一个 sayName 函数了, 这样没有增长功能, 反而更加浪费内存了。

2.2 改变构造函数的原型指向

上节说到经过构造函数产生的实例的原型指向的也是构造函数的原型, 若是令构造函数的原型指向一个新的对象, 那么以后再建立的实例的原型会指向哪呢? 是指向老的原型仍是新的对象呢? 答案是以后建立的实例的原型会指向新的对象, 以前建立的实例的原型会指向旧的对象。下面经过代码来看一下:

function Person(){}
Person.prototype.fn1 = function(){
    console.log('fn1 reporting in old proto');
}

// 建立实例
let oldOne = new Person();

// 修改构造函数的原型指向
Person.prototype = {
    fn2(){  // 新原型对象中定义一个新函数
        console.log('fn2 in new proto');
    }
};

// 建立新实例
let newOne = new Person();

// 在修改原型指向以前建立的实例没法访问新原型中的方法
oldOne.fn1(); // fn1 reporting in old proto
oldOne.fn2(); // TypeError: oldOne.fn2 is not a function
console.log(oldOne.fn2);  // undefined

// 修改以后的原型的实例没法访问旧原型中的方法
newOne.fn1();  // TypeError: newOne.fn1 is not a function
console.log(newOne.fn1);  // undefined
newOne.fn2();  // fn2 in new proto
复制代码

从上面的代码能够看出在建立一个实例时, 会将实例的原型引用设置成构造函数当前的原型上。这样新实例没法访问旧原型里的属性, 旧实例也没法访问新原型里的属性。

上面的代码能够归纳为下图:

3. 实现继承

不少书上都说实现利用原型链实现继承的最佳方案是将一个对象的原型设置成另外一个对象的实例, 即 SubClass.prototype = new SuperClass(), 例如 Student.prototype = new Person()

例以下面的代码:

// 定义类 Person 的构造函数
function Person(){}
Person.prototype.walk = function(){
    console.log('I am walking freely ...');
}

// 定义 Student 类的构造函数
function Student(){}
Student.prototype = new Person(); // 继承 Person

let xm = new Student();
xm.walk();  // I am walking freely ... 成功调用继承来的方法
复制代码

经过上面的代码发现, 将 Student 类构造函数的原型设置成 Person 的一个实例, 能够实现继承。经过原型链能够更加清晰的看出这种继承方式的实现原理:

如上图所示, 令构造函数 Student 的原型指向 Person 的实例, 以后生成的 Student 实例的原型属性就会自动指向 Person 实例了。并且 Person 有的属性, Student 都有了。这就是 Student 的实例 xm 能调用它自身没有的函数 walk 的缘由。

同时因为旧的原型没有被引用, 因此会被清理删除。

可是在图中还能够看到这样实现的继承有个问题: 就是 Student 原本的原型上有个 constructor 属性, 如今没有了。虽然能够经过原型链找到 Person 原型里的 constructor 属性, 可是这并非咱们想要的继承方式。索性这个问题能够用 Object.defineProperty 来解决, 代码以下:

Object.defineProperty(Student.prototype, 'constructor', {
    enumerable: false,  // 设置不可枚举
    value: Student,     // 指向 Student 构造函数
    writable: true      // 可写
});
复制代码

这样, Student 的原型就有了 constructor 属性, 并且指向了它该指向的地方。

4. class 语法糖

ES6 引入的 class 关键字可使继承的实现更加简洁, 并且更加像 Java 之类的经典 OO 语言对类的定义。

4.1 利用 class 关键字建立一个类

class Person {
    constructor(name){  // 构造函数
        this.name = name;
    }

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

let xm = new Person('小明');
xm.sayName();  // '小明'
复制代码

上面的代码定义了一个 Person 类, 并利用 constructor 传入了 name 属性, 并定义了一个 sayName 函数。经过实验说明了这段代码能够正常执行。

上面的这段代码的底层仍然是基于原型实现的, 能够按照文章第 3 部分的内容转换成以下的代码:

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

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

let xm = new Person('小明');
xm.sayName();  // '小明'
复制代码

4.2 利用 class 关键字实现继承

经过文章第3部分能够看出, 实现继承是比较麻烦的事情, 可是利用 class 这个语法糖中的 extendssuper 关键字能够很简洁的实现继承,例以下面的代码:

class Person {
    constructor(name){  // 构造函数
        this.name = name;
    }

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

class Student extends Person {
    constructor(name, grade){  // 构造函数, 传入子类的参数
        super(name);  // 利用 super 关键字调用父类的构造函数
        this.grade = grade;
    }

    getGrade(){  // 定义子类本身的函数
        console.log(this.grade);
    }
}

let xm = new Student('小明', 99);
xm.sayName();  // '小明', 说明能够调用父类的方法
xm.getGrade(); // 99, 正常调用子类的方法
复制代码

参考:
《JavaScript忍者秘籍》
MDN

若有错误,感谢指正~

相关文章
相关标签/搜索