《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 前端开发
如何在 JavaScript 中使用 __proto__ 实现对象的继承?
使用`__proto__`实现对象继承时需要注意原型链的完整性和属性方法的正确继承,避免出现意外的行为和错误。同时,在现代JavaScript中,也可以使用`class`和`extends`关键字来实现更简洁和直观的继承语法,但理解基于`__proto__`的继承方式对于深入理解JavaScript的面向对象编程和原型链机制仍然具有重要意义。
|
机器学习/深度学习 JavaScript 前端开发
JS进阶教程:递归函数原理与篇例解析
通过对这些代码示例的学习,我们已经了解了递归的原理以及递归在JS中的应用方法。递归虽然有着理论升华,但弄清它的核心思想并不难。举个随手可见的例子,火影鸣人做的影分身,你看到的都是同一个鸣人,但他们的行为却能在全局产生影响,这不就是递归吗?雾里看花,透过其间你或许已经深入了递归的魅力之中。
478 19
|
JavaScript 前端开发
JavaScript 原型链的实现原理是什么?
JavaScript 原型链的实现原理是通过构造函数的`prototype`属性、对象的`__proto__`属性以及属性查找机制等相互配合,构建了一个从对象到`Object.prototype`的链式结构,实现了对象之间的继承、属性共享和动态扩展等功能,为 JavaScript 的面向对象编程提供了强大的支持。
|
JavaScript 前端开发
原型链在 JavaScript 中的作用是什么?
原型链是 JavaScript 中实现面向对象编程的重要机制之一,它为代码的组织、复用、扩展和多态性提供了强大的支持,使得 JavaScript 能够以简洁而灵活的方式构建复杂的应用程序。深入理解和熟练运用原型链,对于提升 JavaScript 编程能力和开发高质量的应用具有重要意义。
|
JavaScript 数据可视化 前端开发
three.js简单实现一个3D三角函数学习理解
1.Three.js简介 Three.js是一个基于JavaScript编写的开源3D图形库,利用WebGL技术在网页上渲染3D图形。它提供了许多高级功能,如几何体、纹理、光照、阴影等,以便开发者能够快速地创建复杂且逼真的3D场景。同时,Three.js还具有很好的跨平台和跨浏览器兼容性,让用户无需安装任何插件就可以在现代浏览器上观看3D内容。
483 0
|
JavaScript 前端开发 Java
深入理解 JavaScript 中的 Array.find() 方法:原理、性能优势与实用案例详解
Array.find() 是 JavaScript 数组方法中一个非常实用和强大的工具。它不仅提供了简洁的查找操作,还具有性能上的独特优势:返回的引用能够直接影响原数组的数据内容,使得数据更新更加高效。通过各种场景的展示,我们可以看到 Array.find() 在更新、条件查找和嵌套结构查找等场景中的广泛应用。 在实际开发中,掌握 Array.find() 的特性和使用技巧,可以让代码更加简洁高效,特别是在需要直接修改原数据内容的情形。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一
|
监控 JavaScript 前端开发
MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver:原理与实战案例
MutationObserver 是一个非常强大的 API,提供了一种高效、灵活的方式来监听和响应 DOM 变化。它解决了传统 DOM 事件监听器的诸多局限性,通过异步、批量的方式处理 DOM 变化,大大提高了性能和效率。在实际开发中,合理使用 MutationObserver 可以帮助我们更好地控制 DOM 操作,提高代码的健壮性和可维护性。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
MutationObserver详解+案例——深入理解 JavaScript 中的 MutationObserver:原理与实战案例
|
数据采集 JavaScript 前端开发
一站搞定原型链:深入理解JavaScript的继承机制
综上所述,可以得出: 1. 原型链是对象通过原型实现属性和方法继承的一种机制。 2. 每个对象都有一个 __proto__ 属性,指向它的原型对象。 3. 每个函数(包括构造函数)都有一个 prototype 属性,指向一个对象,这个对象的属性和方法可以被实例共享。 4. 构造函数创建对象时,新对象的 __proto__ 属性指向构造函数的 prototype 对象。 5. 继承可以通过设置原型对象实现,也可以使用 ES6 的 class 语法糖。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一
|
JavaScript 前端开发 Java
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
柯里化是一种强大的函数式编程技术,它通过将函数分解为单参数形式,实现了灵活性与可复用性的统一。无论是参数复用、延迟执行,还是函数组合,柯里化都为现代编程提供了极大的便利。 从 Redux 的选择器优化到复杂的数据流处理,再到深度嵌套的函数优化,柯里化在实际开发中展现出了非凡的价值。如果你希望编写更简洁、更优雅的代码,柯里化无疑是一个值得深入学习和实践的工具。从简单的实现到复杂的应用,希望这篇博客能为你揭开柯里化的奥秘,助力你的开发之旅! 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
623 17