《JS原理学习 (2) 》深入理解原型链与继承(下)

简介: 《JS原理学习 (2) 》深入理解原型链与继承(下)

原型链继承


前面的原理分析章节中,在最后的示意图中,我们很直观的看到了原型链的样子,接下来我们来捋一下原型链的具体概念。


  • 每个构造函数都有一个原型对象
  • 原型对象都包含一个指向构造函数的指针(constructor
  • 每个构造函数的实例都包含一个指向原型对象的内部指针(__proto__
  • 如果让原型对象等于另一个构造函数的实例,此时的原型对象将包含一个指向另一个构造函数原型的指针
  • 相应的,另一个构造函数的原型中也包含着一个指向另一个构造函数的指针
  • 如果另一个原型又是另一个构造函数的实例,那么上述关系依然成立
  • 如此层层递进,就构成了实例与原型的链条,也正是我们示意图中看到的橙色线条,这就是原型链的基本概念


接下来,我们通过一个例子来讲解下原型链的继承,代码如下:


function Super() {
    this.property = true;
}
Super.prototype.getSuperValue = function() {
    return this.property;
}
function Sub() {
    this.subProperty = false;
}
// Sub原型指向Super实例,constructor被重写,指向Super
Sub.prototype = new Super();
Sub.prototype.getSubValue = function () {
    return this.subProperty;
}
let sub = new Sub();
console.log("获取Super的属性值", sub.getSuperValue());
console.log("sub实例的原型对象等于Sub构造函数的原型对象", sub.__proto__ === Sub.prototype);
console.log("Sub构造函数的原型对象的原型对象等于Super构造函数的原型对象", Sub.prototype.__proto__ === Super.prototype)
console.log("Sub构造函数的原型对象constructor指向Super的构造函数", Sub.prototype.constructor === Super)


运行结果如下:


640.png

                             image-20210312215707154


  • 首先,我们创建了一个名为Super的函数,内部添加了一个名为property的属性,值为true
  • 随后,在Super的原型对象上添加了getSuperValue方法,返回函数内部的property属性
  • 随后,我们创建了一个名为Sub的函数,内部添加了一个名为subProperty的属性,值为false
  • 随后,我们将Sub的原型对象指向Super实例,此时就实现了继承,Sub原型上将会拥有Super原型上的方法
  • 随后,我们在Sub原型对象添加了getSubValue方法,返回函数内部的subProperty属性
  • 最后,我们实例化Sub对象,它与Super对象之间就组成了一条原型链,符合我们在原理解析中所讲的关系


接下来,我们将上述分析内容画成图,更好理解一点,如下所示(橙色线条为原型链):


640.png

                           image-20210311112200899


存在的问题


我们使用原型链来实现继承时,如果继承了原型对象上的引用类型,那么这个引用类型将会被所有实例共享。


接下来我们通过一个例子来说明下这个问题:


function Super() {
    this.list = ["a","b","c"];
}
function Sub() {
}
Sub.prototype = new Super();
const sub1 = new Sub();
sub1.list.push("d");
console.log(sub1.list);
const sub2 = new Sub();
console.log(sub2.list);


上述代码中:


  • 首先,声明了两个构造函数Super、sub
  • 随后,将Sub的原型对象指向Super的实例,实现继承
  • 随后,实例化Sub对象得到sub1实例
  • 向sub1实例的list中添加一个元素d
  • 此时,sub1实例的list数组元素为[ 'a', 'b', 'c', 'd' ]
  • 随后,再次实例化Sub对象得到sub2实例
  • 此时,sub2实例的list数组元素为[ 'a', 'b', 'c', 'd' ]


运行结果如下:


640.png

                                image-20210311141113229


问题很明显了,我们没有向sub2的list数组中添加元素,我们希望它的值是Super原型上定义的["a","b","c"]


由于Super构造函数中定义的list属性是引用类型,因此在实例化时它被共享了,sub1实例改了它的值后,sub2实例化时拿到的就是sub1实例改后的值,即[ 'a', 'b', 'c', 'd' ]


在下个章节中,我们将解决引用类型被实例共享的问题


构造函数继承


我们在子类的构造函数中,可以使用call将父类构造函数的所有属性和方法拷贝到当前构造函数中,这样我们在实例化后,修改属性和方法就是修改的复制过来的内容了,不会影响父类构造函数中的内容。


接下来,我们通过一个例子来讲解下上述话语:


function Super() {
    this.list = ["a","b","c"];
}
function Sub() {
    Super.call(this)
}
const sub1 = new Sub();
sub1.list.push("d");
console.log("sub1" ,sub1.list);
const sub2 = new Sub();
console.log("sub2", sub2.list);


上述代码,我们沿用的上个章节的的例子,这里只讲改变的部分:


  • 我们在Sub的构造函数中使用call将Super中的属性和方法拷贝了过来,实现了继承
  • 由于属性在每次实例化时,都会拷贝属性到当前实例,因此sub1中添加的元素不会影响到sub2


运行结果如下:


640.png

                              image-20210312112758516


存在的问题


我们借用构造函数实现继承,无法继承父类原型上的方法和属性,那么函数的复用性也就就没了。


我们继续拿上个章节的代码来举例:


function Super() {
    this.list = ["a","b","c"];
}
Super.prototype.newList = [];
function Sub() {
    Super.call(this)
}
const sub1 = new Sub();
console.log("sub1" ,sub1.newList);


  • 我们向Super的原型上添加了newList属性
  • 在sub1实例中访问时是不存在的


运行结果如下:


640.png

                              image-20210311160013667


组合继承


经过前两个章节的分析,我们知道了原型链继承方式可以继承原型对象上的属性和方法,构造函数继承可以继承构造函数中的属性和方法,他们彼此互补,那么我们将它俩的长处结合起来,就实现了组合继承,也完美的弥补了它们各自的短板。


接下来,我们通过一个例子来讲解下组合继承:


// 组合继承
function Super(name) {
    this.name = name;
    this.list = ["a","b","c"];
}
Super.prototype.getName = function () {
    return this.name;
}
function Sub(name, age) {
    // 构造函数继承,第二次调用父类构造函数
    Super.call(this,name);
    this.age = age;
}
// 原型链继承,第一次调用父类构造函数
Sub.prototype = new Super();
Sub.prototype.constructor = Sub;
Sub.prototype.getAge = function () {
    return this.age;
}
const sub1 = new Sub("神奇的程序员","20");
sub1.list.push("d");
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
console.log("sub1", sub1.list);
const sub2 = new Sub("大白","20");
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
console.log("sub2", sub2.list);


上述代码中:


  • 首先,创建了一个名为Super的函数,接受一个name参数,构造函数中有两个属性name和list
  • 随后,我们向Super的原型对象上添加getName方法,返回Super中的name属性值
  • 随后,我们创建一个名为Sub的函数,接受两个参数:name、age,在构造函数中添加age属性,继承父类构造函数中的属性与方法
  • 随后,我们重写Sub构造函数的原型对象为Super的实例,修正constructor指向
  • 随后,我们向Sub的原型对象上添加getAge方法,返回Sub中的age属性
  • 最后,我们实例化两个Sub构造函数的实例,测试继承下来的方法


运行结果如下:


640.png

                          image-20210311175629506


寄生式组合继承


我们在实现组合继承时,调用了两次父类的构造函数。


第一次调用父类构造函数时:


  • 我们重写了Sub的原型对象,将其指向了Super的实例
  • 此时,父类构造函数实例上的属性和方法会赋值给Sub.prototype


这一次调用就是我们在原型链继承章节所讲的内容,子类已经继承了父类构造函数中的属性与方法和原型对象上的属性与方法。


第二次调用父类构造函数时:


  • 我们在Sub的构造函数内部,使用call会将Super中的属性和方法赋值给Sub的实例上
  • 原型链在搜索属性时,实例上的属性会屏蔽原型链上的属性


因此,第二次调用时,我们就没有必要将构造函数中的属性和方法赋值给Sub构造函数的实例了,这是无意义的。


接下来,我们来看下优化后的组合继承:


function Super(name) {
    this.name = name;
    this.list = ["a", "b", "c"];
}
Super.prototype.getName = function () {
    return this.name;
}
function Sub(name, age){
    Super.call(this, name);
    this.age = age;
}
// 创建一个中间函数,用于继承Super的原型对象
function F() {
}
// 将F的原型对象指向Super的原型对象
F.prototype = Super.prototype;
// 将Sub的原型对象指向F的实例
Sub.prototype = new F();
Sub.prototype.constructor = Sub;
Sub.prototype.getAge = function () {
    return this.age;
}
const sub1 = new Sub("神奇的程序员","20");
sub1.list.push("d");
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
console.log("sub1", sub1.list);
const sub2 = new Sub("大白","20");
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
console.log("sub2", sub2.list);


上述代码中:


  • 我们先创建了一个中间函数F
  • 随后,重写了F的原型对象,将F的原型对象直接指向了Super的原型对象
  • 最后,我们将Sub的原型对象指向F的实例,从而实现原型链继承。


他的高效之处在于只有在实例化时才会调用一次Super构造函数,并且保持原型链的不变。


运行结果如下:


640.png

                       image-20210311212111885


优化后的组合继承又名寄生式组合继承,在上面的实现代码中,我们使用一个中间函数实现原型链的继承,这个中间函数也可以可以使用Object.create()来替代,他们的实现原理都一样。

那么,在重写Sub构造函数的原型对象时,我们就可以这样写:Sub.prototype = Object.create(Super.prototype, {constructor: {value: Sub}})


修改原型对象指向实现继承


上个章节中,我们使用一个中间函数实现了原型链的继承。我们还可以直接将子类的原型对象通过__proto__属性将其指向父类的原型对象,这种方式没有改变子类的原型对象,所以子类原型对象上的constructor属性还是指向父类的构造函数。


接下来,我们通过一个例子来讲解下上述话语:


function Super(name) {
    this.name = name;
    this.list = ["a", "b", "c"];
}
Super.prototype.getName = function () {
    return this.name;
}
function Sub(name, age){
    Super.call(this, name);
    this.age = age;
}
// 修改Sub构造函数的原型对象指向改为Super的原型对象
Sub.prototype.__proto__ = Super.prototype;
Sub.prototype.getAge = function () {
    return this.age;
}
const sub1 = new Sub("神奇的程序员","20");
sub1.list.push("d");
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
console.log("sub1", sub1.list);
const sub2 = new Sub("大白","20");
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());
console.log("sub2", sub2.list);


上述代码中:


  • 我们使用__proto__属性修改了Sub原型对象指向


原理解析章节中,我们知道每一个除null外的javascript对象都有一个__proto__属性,默认指向这个对象的原型对象,因此我们可以通过这个属性来修改它指向的原型对象。


我们还可以使用ES6中的Object.setPrototypeOf()方法来修改对象的原型。

那么,上述例子中的代码就可以改为:Object.setPrototypeOf(Sub.prototype, Super.prototype)


构造函数的静态方法继承


直接向构造函数上添加一个方法,这个方法就是静态方法


我们前面讲的所有的继承方法,都没有实现构造函数上的静态方法继承,然而在ES6的class继承中,子类是可以继承父类的静态方法的。


我们可以通过Object.setPrototypeOf()方法实现静态方法的继承。


接下来,我们通过一个具体的例子来讲解下:


function Super(name) {
    this.name = name;
    this.list = ["a", "b", "c"];
}
Super.prototype.getName = function () {
    return this.name;
}
// 添加静态方法
Super.staticFn = function () {
    return "Super的静态方法";
}
function Sub(name, age) {
    // 继承Super构造函数中的数据
    Super.call(this, name);
    this.age = age;
}
// 修改Sub的原型指向
Object.setPrototypeOf(Sub.prototype, Super.prototype);
// 继承父类的静态属性与方法
Object.setPrototypeOf(Sub, Super);
Sub.prototype.getAge = function () {
    return this.age;
}
console.log(Sub.staticFn());
const sub1 = new Sub("神奇的程序员", "20");
sub1.list.push("d");
console.log("sub1", sub1.list);
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
const sub2 = new Sub("大白", "20");
console.log("sub2", sub2.list);
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());


运行结果如下:


640.png

                         image-20210312000444311


上述代码中的其他部分与之前举的例子相同,我们来分析下不同之处:


  • 首先,我们向Super构造函数添加了一个名为staticFn的静态方法
  • 随后,我们通过Object.setPrototypeOf(Sub, Super)函数继承了Super构造函数的静态属性与方法


至此,我们就实现了一个完美的继承,也正是ES6的class语法糖的底层实现。


class语法糖


ES6中新增了一个class修饰符,我们用class修饰符创建两个对象后,我们就可以使用extends关键词来实现继承了。它的底层实现就是我们上面所讲的寄生式组合继承结合构造函数的静态方法继承来实现的。


接下来,我们来看下上个章节中举的例子,如何使用class实现,代码如下:


class Super {
    constructor(name) {
        this.name = name;
        this.list = ["a","b","c"];
    }
    getName() {
        return this.name;
    }
}
// 向Super添加静态方法
Super.staticFn = function () {
    return "Super的静态方法";
}
class Sub extends Super{
    constructor(name, age) {
        super(name);
        this.age = age;
    }
    getAge() {
        return this.age;
    }
}
console.log(Sub.staticFn())
const sub1 = new Sub("神奇的程序员", "20");
sub1.list.push("d");
console.log("sub1", sub1.list);
console.log("sub1", sub1.getName());
console.log("sub1", sub1.getAge());
const sub2 = new Sub("大白", "20");
console.log("sub2", sub2.list);
console.log("sub2", sub2.getName());
console.log("sub2", sub2.getAge());


运行结果如下:


640.png

                             image-20210312004416434


代码地址


本系列文章的所有示例代码,请移步:js-learning


写在最后


本文为《JS原理学习》系列的第2篇文章,本系列的完整路线请移步:JS原理学习 (1) 》学习路线规划

  • 公众号无法外链,如果文中有链接,可点击下方阅读原文查看😊
相关文章
|
JavaScript 前端开发 Java
深入JS面向对象(原型-继承)(一)
深入JS面向对象(原型-继承)
31 0
|
1月前
|
JavaScript 前端开发
js开发:请解释原型继承和类继承的区别。
JavaScript中的原型继承和类继承用于共享对象属性和方法。原型继承利用原型链查找属性,节省内存但不支持私有成员。类继承通过ES6的class和extends实现,支持私有成员但占用更多内存。两者各有优势,适用于不同场景。
19 0
|
1月前
|
自然语言处理 JavaScript 前端开发
探索JavaScript中的闭包:理解其原理与实际应用
探索JavaScript中的闭包:理解其原理与实际应用
19 0
|
1月前
|
JavaScript
JS数组增删方法的原理,使用原型定义
JS数组增删方法的原理,使用原型定义
|
3天前
|
JavaScript 前端开发 测试技术
学习JavaScript
【4月更文挑战第23天】学习JavaScript
11 1
|
4天前
|
前端开发 JavaScript 编译器
深入解析JavaScript中的异步编程:Promises与async/await的使用与原理
【4月更文挑战第22天】本文深入解析JavaScript异步编程,重点讨论Promises和async/await。Promises用于管理异步操作,有pending、fulfilled和rejected三种状态。通过.then()和.catch()处理结果,但可能导致回调地狱。async/await是ES2017的语法糖,使异步编程更直观,类似同步代码,通过事件循环和微任务队列实现。两者各有优势,适用于不同场景,能有效提升代码可读性和维护性。
|
7天前
|
JavaScript
什么是js的原型链
什么是js的原型链
|
11天前
|
JavaScript 前端开发 应用服务中间件
node.js之第一天学习
node.js之第一天学习
|
1月前
|
运维 JavaScript 前端开发
发现了一款宝藏学习项目,包含了Web全栈的知识体系,JS、Vue、React知识就靠它了!
发现了一款宝藏学习项目,包含了Web全栈的知识体系,JS、Vue、React知识就靠它了!
|
1月前
|
JavaScript
Vue.js学习详细课程系列--共32节(4 / 6)
Vue.js学习详细课程系列--共32节(4 / 6)
35 0