[前端漫谈_6] 从原型聊到原型继承,深刻理解 JavaScript 面向对象精髓

前言

开头说点题外话,不知道何时开始,我发如今 JavaScript 中,你们都喜欢用 foo 和 bar 来用做示例变量名,为此专门查了一下这家伙的 来源javascript

“The etymology of foo is obscure. Its use in connection with bar is generally traced to the World War II military slang FUBAR, later bowdlerised to foobar. ... The use of foo in a programming context is generally credited to the Tech Model Railroad Club (TMRC) of MIT from circa 1960.”html

foo的词源是模糊的。 它与bar的关系能够追溯到第二次世界大战的军事俚语 FUBAR,后简化为foobar。 而在编程环境中使用 foo 一般认为起源于约 1960 年时麻省理工学院的技术模型铁路俱乐部(TMRC)。前端

okay ,那么今天,咱们也看看这段 Foo 的代码来聊聊原型。java

function Foo(name){
    this.name = name
}

let foo = new Foo('demoFoo')

console.log(foo.name) // demoFoo
复制代码

1. 为何 JavaScript 被设计成基于原型的模式?

你们都知道 Java 做为面向对象的语言的三个要素:封装继承多态,我以前也写过 Java ,因此在一开始学习 JavaScript 的时候,我老是会去经过类比熟悉的 Java 来理解 JavaScript 中关于继承的概念,可是不管怎么去类比都以为不是那么回事,由于这自己就是两种彻底不一样的方式。编程

JavaScript 是如何设计出来的呢,wiki 是这样说的函数

“网景决定发明一种与 Java 搭配使用的辅助脚本语言,而且语法上有些相似”学习

无论怎么说,JavaScript 在设计之初都受到了 Java 的影响,因此在 Javascript 中也有了对象的概念,可是做为一种 辅助脚本语言 ,类的概念有些过于笨重,不够简单。可是对象之间须要一种让彼此都产生联系的机制,怎么办呢?ui

依旧是参考了 Java 的设计,Java 中生成一个对象的语法是这样:this

Foo foo = new Foo() // 请注意这里的Foo 指的是类名,而不是构造函数。
复制代码

因而 Brendan Eich 也模仿了这样的方式使用了 new 来生成对象,可是 JavaScript 中的 new 后面跟的不是 Class 而是 Constructorspa

okay 解决了实例化的问题,可是仅仅只靠一个构造函数,当前对象没法与其余的对象产生联系,例若有的时候咱们指望共享一些属性:

function People(age) {
    this.age = age
    this.nation = 'China'
}

// 父子大小明
let juniorMing = new People(12)
let seniorMing = new People(38)

// 有天他们一块儿移民了,此时我想改变他们的国籍为 America
juniorMing.nation = 'America'
// 可是改变小明一人的国籍并不能影响大明的国籍
console.log(seniorMing.nation) // China
复制代码

(nation)国籍 在这个例子中成为了咱们想在两个对象之间共享的属性,可是因为没有类的概念,必须得有一个新的机制来处理这部分 须要被共享的属性。这就是 prototype 的由来。

因此咱们上面的例子变成了什么呢?

function People(age) {
    this.age = age
}

People.prototype.nation = 'China'

// 父子大小明
let juniorMing = new People(12)
let seniorMing = new People(38)

// 有天他们一块儿移民了,此时我想改变他们的国籍为 America
People.prototype.nation = 'America'

console.log(seniorMing.nation) // America
console.log(juniorMing.nation) // America
复制代码

2. 最简单的原型

结合前言部分中的代码:

function Foo(name){
    this.name = name
}

let foo = new Foo('demoFoo')

console.log(foo.name) // demoFoo
复制代码

先提取一下关键信息:

  • foo 是被构造出来的实例对象。
  • foo 的构造方法是 Foo()

因此最基础的原型链就是这样:

在这个例子中 Constructor.prototype 等价于 Foo.prototype

构造函数 Foo 能够经过 Foo.prototype 来访问原型,同时被构造出来的对象 foo 也能够经过 foo.__proto__ 来访问原型:

Foo.prototype === foo.__proto__ // true
foo.__proto__.constructor === Foo // true
复制代码

简单的来讲,Foo 函数,参照了 Foo.prototype 生产出来了一个 foo 对象。

为了更好的理解这一过程,我得从一个故事开始提及:

  • 在好久好久好久之前,有一个工匠偶然间看到了一个很美的古迹雕像(原型 Foo.prototype
  • 他想经过批量的生产复刻的版原本发家致富,因而他先分析雕像,还原了制造的过程,而且设计出一条生产线(构造器 Foo
  • 而后经过这个构造器,能够源源不断的造出许多的复刻雕像(实例 foo)。

3.原型链

刚刚的故事尚未结束,后来一天这个工匠开始思考,以前看到的那个雕塑是哪里来的呢?又是怎么作出来的呢?就算是自然造成的,那又是什么条件造成了这样的雕塑呢?

带着这些问题,他开启了 996 模式,寻师访友,查阅典籍,经历了多年苦心研究,终于有了新的发现:

① 原来他做为参照物(原型)的雕像 ( Foo.prototype / foo.__proto__ ) 是 n 年前一位雕刻大师参照天然的现象( Object.prototype )而后设计了铸造方式( Object() )造出来的。

从代码来看:

foo.__proto__.__proto__ === Object.prototype //true

Foo.prototype.__proto === Object.prototype //true

Object === Object.prototype.constructor
复制代码

用图来描述这一过程就是:

② 除此以外,他发现,原来这位雕刻大师设计的铸造方式( Object() ),是根据天然现象的造成规律( Function.prototype )来设计的。因此,本质上来讲,他所设计出的生产线( Foo() ),也间接的参考了天然现象的造成规律( Function.prototype )

console.log(Object.__proto__=== Function.__proto__)
console.log(Foo.__proto__ === Function.prototype)
复制代码

而后咱们来看看图会更加清晰一些:

③ 故事到这里尚未结束,这位工匠发现,原来,对于天然现象造成规律的描述(Function.prototype)是先辈们从这天然现象( Object.prototype )中总结出来的。

因此咱们从代码中看到:

Function.prototype.__proto__ === Object.prototype
复制代码

因此 人法地、地法天、天法道、道法天然,如今咱们能够看到完整的原型链:

④ 故事并无像咱们想象中那样结束,这位工匠最后改良了生产链,结合了先人的方式和关于这一天然现象的规律,从新定义了关于这一规律的描述。

因此代码被改写为:

let foo = new Object()
console.log(foo.__proto__ === Object.prototype) // true

复制代码

而这一故事也一直流传到今天:

4.原型继承

看到这里,相信你对 JavaScript 中的原型和原型链,都有了新的认识,那么咱们再来聊聊原型继承,在聊原型继承以前,咱们想一想什么叫作继承呢?

抛开计算机中的理论,咱们就说一个最简单的例子,小王经过继承了他老爸的遗产走上了人生巅峰。这里面其实有一个关键信息:他老爸的遗产帮助他更快的走上了人生巅峰,换言之,小王不须要本身努力也能经过他老爸留下的财产走上人生巅峰。

听起来很像是废话,可是本质也在这里:

  • 咱们定义了一个新的对象 child
  • 可是咱们并不想再帮他定义其余复杂的属性
  • 因此咱们选择了一个以前定义过的构造函数 parents 而后让 child 直接从 parents 那里把全部东西都继承过来。
  • parents 的基础上,我可能为 child 定制了一些内容,因此之后有可能直接从 child 来继承这一切。

听起来继承就像是 让一个普通的对象快速的得到本来不属于它的超能力。

蝙蝠侠的故事告诉咱们:rich 也是一种超能力。

okay 让咱们看看怎么样让 JavaScript 中的对象快速得到超能力呢?结合咱们上面了解的内容,咱们会发现几个很关键的点,若是要让一个对象得到超能力,只有下面的三种途径:

  1. 由于对象是由 constructor 生产的,因此咱们能够经过改变 constructor 来实现。
  2. constructor 有一个关键的属性: constructor.prototype 因此改变原型也能达到目的。
  3. 直接改变对象的属性,将要添加的内容复制过来。

不管你怎么去找继承的方法,继承的本质就在这里,领悟本质会让问题变得简单,因此咱们一块儿来看看具体的实现。

改写构造函数

咱们从最简单的开始,这种方式的核心就在于直接在当前构造函数中,调用你想继承的构造函数。

function Student(){
    this.title = 'Student'
}

function Girl(){
    Student.call(this)
    this.sex = 'female'
}

let femaleStudent = new Girl()

console.log(femaleStudent)
复制代码

这种方式并无影响到你的原型链,由于本质上来讲,你仍是经过 Girl() 来生成了一个对象,而且 Girl.prototype 也并未受到影响,因此原型链不会产生变化。

改变 constructor.prototype

改变 constructor.prototype 这一方式有不一样的状况,咱们能够分开来看

① chidConstructor.prototype = new Parents()

这种方式的核心已经写在了标题上,因此咱们来看看代码吧:

function Parent() {
    this.title = "parent"
}
function Child() {
    this.age = 13
}
const parent = new Parent()
Child.prototype = parent
const child = new Child()
console.log(child);
console.log('child.__proto__.constructor: ', child.__proto__.constructor);
console.log('parent.constructor: ', parent.constructor);//每个实例也有一个constructor属性,默认调用prototype对象的constructor属性
复制代码

打印出来是什么呢?

把他和本来没有继承以前的 child 对比一下:

结论是:

  • 当前对象的原型已经被重置为一个 parent 对象
  • 当前对象的构造方法由 Child() 变成了 Parent()
  • 本来的 child.prototype 被替换为 parent 对象后与构造器之间的联系成为了单向:
console.log(parent.constructor.prototype === parent) //false
复制代码

咱们用图来描述一下:

为了解决上面存在的问题,咱们改写了代码,添加了一行

...
const parent = new Parent()
Child.prototype = parent
Child.prototype.constructor = Child; //添加了这行
const child = new Child()
...
复制代码

因此原型链成为下面这样:

有的同窗可能会不理解为何 Child.prototype === parent 以及 parent.constructor === Child()

Child.prototype === parent 是由于咱们在代码中中强行设置了,parent.constructor === Child() 是由于 parent 对象自己也有一个 constructor 属性,这个属性默认返回 parent.__proto__.constructor 因此以前是 Parent() 可是如今也被代码强制设置为了 Child()

Child.prototype = parent
Child.prototype.constructor = Child; // parent.constructor === Child
复制代码

② 方法 ① 和 改变构造函数的组合

咱们把前面两种方式组合起来:

function Parent() {
    this.title = "parent"
}
function Child() {
    Parent.call(this)
    this.age = 13
}
const parent = new Parent()
Child.prototype = parent
Child.prototype.constructor = Child;
const child = new Child()
console.log(child);
复制代码

打印的结果是:

这样咱们生成的 child 对象自己包含了他从 Parent 中继承来的 title 属性,可是同时 Child.prototype 同时也包含了全部 Parent 上的全部属性,形成内存的浪费:

③ 方法 ② 组合改进

因此咱们把方法 ② 改变一下,避免内存的浪费,既然缘由是由于咱们将 Child.prototype设置为 new Parent() 的过程当中,使用 Parent() 进行实例化因此将属性都继承到了 Child 原型上,那么为何不能够直接使用原型对原型进行赋值呢?

也就是 chidConstructor.prototype = Parents.prototype

function Parent() {
    this.title = "parent"
}
function Child() {
    Parent.call(this)
    this.age = 13
}
Child.prototype = Parent.prototype
Child.prototype.constructor = Child;
const child = new Child()
console.log(child);
复制代码

咱们看下打印信息:

okay,关键的信息看起来都很完美,再分析下原型链:

嗯,单纯站在 child 的角度来看好像没有什么问题,可是若是咱们打印下面这几行会发现问题:

console.log('parent: ', new Parent());
console.log(Child.prototype === Parent.prototype) //true
复制代码

Parent.prototype 的构造器变成了 Child 是不合理的,并且此时 Child.prototype === Parent.prototype 两个属性指向同一个对象,当咱们改变 Child.prototype 的时候,咱们并不但愿影响到 Parent.prototype 可是在这里成为了避免可避免的问题。

那有什么办法能够解决这个问题呢?

④ 拷贝 prototype

若是咱们并不直接将 Parent.prototype 赋值给 Child.prototype 而是复制一个如出一辙的新对象出来替代呢?

function Parent() {
    this.title = "parent"
}
function Child() {
    Parent.call(this)
    this.age = 13
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child;//咱们在这里修改了构造器的指向,你一样能够在Object.create 方法中作这件事。
const child = new Child()
console.log('parent: ', new Parent());
console.log(child);
复制代码

最后打印出来的结果如何呢?

没有任何问题,避免了父子原型的直接赋值致使的各类问题~

⑤ 空对象法

除了上面的解决方案,还有没有别的办法呢?答案是有的,除了经过复制创造一个新的原型对象,咱们还能够用一个中间函数来实现这件事:

...
function extend(Child, Parent) {
    var X = function () { };
    X.prototype = Parent.prototype;
    Child.prototype = new X();
    Child.prototype.constructor = Child;
}
...
复制代码

一样很完美。这也是 YUI 库实现继承的方式。

直接改变对象的属性

咱们再也不基于原型去玩什么花样,而是直接把整个父对象的属性都拷贝给子对象。若是仅仅是值类型的话,是没有问题的,但若是这时候,父对象的属性中本来包含的是引用类型的值呢?

咱们就要考虑把整个引用类型的属性拷贝一份到子对象,这里就设计到浅拷贝和深拷贝的内容啦~

5.最后

若是你仔细的读完本文,相信你对 JavaScript 中的原型,原型链,原型继承,会有新的认识。

若是以为对你有帮助,记得 点赞 哦,

毕竟做者也是要恰饭的。

很是感谢 阮一峰 老师关于继承整理。

本来想写写静态分析,可是写起来发现本身还有不少的不足,因此这里也不说下次会写啥了,由于太渣也不必定写得出来...

这里是 Dendoink ,奇舞周刊原创做者,掘金 [联合编辑 / 小册做者] 。

对于技术人而言: 是单兵做战能力, 则是运用能力的方法。驾轻就熟,出神入化就是 。在前端娱乐圈,我想成为一名出色的人民艺术家。

扫码关注公众号 前端恶霸 我在这里等你:

相关文章
相关标签/搜索