公有属性继承的7种方式总结(继承系列-4)

前端印记

共 6611字,需浏览 14分钟

 ·

2021-10-12 11:56


继承的几种方式分析

用自己的理解,系统整理下以上几种继承,从简至更简更高级。总结如下:

  • 前言:

  • 1、共享原型的继承方式

  • 2、原型链继承的方式

  • 3、圣杯前身

  • 4、圣杯模式的继承

  • 5、高级模式:变废为宝

  • 6、es6中的继承 - setPrototypeOf

    • 仿写实现:

    • 扩展:Object.getPrototypeOf(b.prototype)

  • 7、Object.create()

  • 公有属性继承的7种方式 - 关键代码总结

  • 总结一下

  • 最后验证代码

  • 思考


前言:

接下来简写字母表示的含义如下:

  • A表示下例代码中的Person构造函数,
  • A实例化对象a = new Person() = person,
  • B表示下例代码中的Man构造函数
  • B实例化对象b = new Man() = man,
  • 中转Temp函数不变
/* 构造函数 */
function Person(name{
 this.name = name; //私有属性
 return this;
}
Person.prototype.eat = function (//公有属性/方法
 console.log('I can eat');
 return this;
}
var person = new Person('构造函数');
console.log(person);
function Man(sex{
 Person.call(this'继承私有属性'); //借用构造函数的方式实现其他对象私有属性的继承。
 this.sex = sex;
}

1、共享原型的继承方式

比如让B.prototype = A.prototype
这么做的缺点是大家成了绑在一条绳上的蚂蚱,一损俱损、一改都改。
且B构造出来的实例化对象的constructor成了A,而不是B了。这就乱套了,瞎认祖宗了。

2、原型链继承的方式

比如让B.prototype = new A()
实现原理是原型指向构造函数的实例化对象。通过实例化对象的__proto__去得到构造函数的原型。
缺点也是B构造出来的实例化对象的constructor成了A,而不是B了。这就乱套了,认贼作父了。
另外缺点还有A构造函数的实例化对象身上的私有属性也被继承了。

所谓“认贼作父”:

A的实例化对象的__proto__指向的就是A的原型,A原型上的constructor指向A本身这本没有错。

但因为你把人家B的原型修改了,B的原型成了A的实例化对象,B构造出来的实例化对象b的__proto__就是A构造出来的实例化对象a,因此导致顺着原型链的关系,A成了B实例化对象b的构造类。但实际b的构造类应该是B。

3、圣杯前身

为了解决以上问题,新方案是建一个第三方Temp函数中转一下。

让 Temp.prototype = A.prototype;
让 B.prototype = new Temp();

弊端:这样还是改变不了B的实例化对象得到的constructor指向A这种认贼作父的局面。

为什么指向不对?
原因很简单:B的实例化对象(b)沿着__proto__查找constructor
b.__proto__ == B.prototype = new Temp(),而new Temp得到的是个实例化对象,普通对象身上没有constructor,只有原型对象身上有。
所以还得沿着原型链找到Temp实例化对象的__proto__,即Temp.prototype,而Temp.prototype == A.prototype,这次终于找到了prototype原型对象、也找到了constructor。但是遗憾的是,这个prototype是A的,A.prototype.constructor === A;,所以最后结果还得是A。

4、圣杯模式的继承

没办法了,我们知道constructor是谁,但是JS引擎搞混了。
所以,只能在上一个基础上,人为的去修改constructor,并将这块代码封装起来,得到终极的封装代码“圣杯模式”:

(function ({
  var Inherit = function ({};
  Inherit.prototype = Person.prototype;
  /* 上一句可以优化,可以利用Object.create简化这一步。改成:
    var protoType = Object.create(Person.prototype);
    Man.prototype = protoType; 
    这样写, 也就是下边第7条的意思
  */

  Man.prototype = new Inherit();
  Man.prototype.constructor = Man; //在prototype添加固定属性,中途拦截一下constructor
  Man.prototype.uber = Person.prototype; // 设置他的超类
}());

5、高级模式:变废为宝

prototype.__proto__指向的是Object.prototype,但Object原型上的内容我们基本不会用、且每个对象原型链的最后都是Object的原型,即使被改了也能最终找到Object原型。那既然这样,为何不从Object的原型下手改一下?

B原型上实例化对象指向A原型Man.prototype.__proto__ = Person.prototype;这堪比将黄河改道啊!简直不要太高级。直呼内行!

冷静一下啊,缺点还是有的,就是__proto__作为隐式属性,是系统自己的属性,不建议我们去修改,如果理解不透彻容易改错,所以不推荐使用。

这么好的思路不让用岂不是可惜?

不必悲伤,因为官方内部利用这个原理帮我们实现了:

6、es6中的继承 - setPrototypeOf

该方法就是让B的原型指向A的原型。

代码如下

Object.setPrototypeOf(B.prototype, A.prototype)

其原理同第五条,只不过是es6给我们新增的官方用法。更安全、更可靠。

仿写实现:

Object.setPrototypeOf = function(_pro, proto){
 _pro.__proto__ = proto;
 return _pro;
}

扩展:Object.getPrototypeOf(b.prototype)

MDN:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf

7、Object.create()

有了上一个Object的方法,联想到另一个es5的Object.create()

本意用来创建一个新对象,第一个参数是一个对象,用来当做新对象的__proto__,第二个参数是一个配置对象,和Object.defineProperties()的第二个参数一致。

因此,我们修改原型指向也可以用这个方法创建出来的对象:

Man.prototype = Object.create(Person.prototype, {
 constructor: {
  value: Man
 }
});

公有属性继承的7种方式 - 关键代码总结

1、原型直接指向原型,简单粗暴

Man.prototype = Person.prototype;

2、原型指向实例化对象

Man.prototype = new Person('继承第二种');

3、中转函数,粗糙圣杯

function Temp(){};
Temp.prototype = Person.prototype;
Man.prototype = new Temp();

4、精美圣杯模式

(function(){
 var Inherit = function(){};
 Inherit.prototype = Person.prototype;
 Man.prototype = new Inherit();
 Man.prototype.constructor = Man;// 在prototype添加固定属性,中途拦截一下constructor
 Man.prototype.uber = Person.prototype; // 设置他的超类
}());

5、高级模式:直接改变原型上的隐式原型

Man.prototype.__proto__ = Person.prototype;

6、Object.setPrototypeOf(B.prototype, A.prototype);

Object.setPrototypeOf(Man.prototype,Person.prototype); // 第五条原理的官方实现

7、Object.create(A.prototype,{…})

Man.prototype = Object.create(Person.prototype, { // class中的继承原理写法
 constructor: {
  value: Man
 }
});

总结一下

从第五条开始,这个思路高明的地方所在:
这么做也很合理,因为man.__proto__指向Man.prototype,还有自己的使命不能被直接替换,但是Man.prototype.__proto__作为Object.Prototype貌似除了要用Object定义的方法外没啥作用,所以从这一骨节上嫁接一下,把Person.prototype按到这里,幸运的是,Person.prototype.__proto__也有Object.prototype,所以我们不仅赚了夫人,也没赔了兵。

最后验证代码

分别用上边的方案实现继承后,可以用下边的代码验证下效果。

var man = new Man('male');
console.log(man);
console.log(man.constructor);

思考

这些方法其实都很麻烦,为什么还要人为的去搞一下继承?
好像要科学方法改细胞似的。就不能天生继承吗?
这就是class出现的原因了。
class写法更简单,目的更明确。
具体写法和做法,可以看后续es6 - 《class》篇章


愿你历尽千帆,归来仍是少年。


让我们一起携手同走前端路!

关注公众号回复【加群】即可

● 工作中常见页面布局的n种实现方法

● 三栏响应式布局(左右固宽中间自适应)的5种方法

● 两栏自适应布局的n种实现方法汇总

● 工作中常见的两栏布局案例及分析

● 垂直居中布局的一百种实现方式

● 常用九宫格布局的几大方法汇总

● 为什么操作DOM会影响WEB应用的性能?

● 移动端滚动穿透的6种解决方案

● Vue + TypeScript 踩坑总结

浏览 10
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报