JavaScript 原型
Opened this issue · 0 comments
目录
- 面向对象
- 原型 & 原型链
- 实现
面向对象
面向对象三大特性:封装、继承、多态
多数的程序语言借助 类 的概念来实现面向对象编程
在 JavaScript
中没有经典类的概念:
- 封装可以借助函数实现
- 多态可以借助
JavaScript
this
的特性来实现
可以通过其他的语言特性实现面向对象的封装与多态
继承基本上只能通过 mixins
混合这种方式来处理,同时还伴随着覆盖、先后执行的问题,不是优雅高效的解决方案
JavaScript
通过原型优雅解决了继承的实现问题
原型 & 原型链
JavaScript 中没有类的概念,创建原型对象是基于函数与 new
关键词
function Cat () { this.type = 'cat' }
const cat = new Cat()
一般这种被 new
执行的函数被称为 构造函数
new
关键词做了什么?(new
关键词后面的函数一般称为构造函数)
- 创建空对象
new Object()
或者{}
- 设置对象
__proto__
属性指向构造函数的原型对象 - 以该对象为
this
执行构造函数 - 关注构造函数执行的返回值
- 存在返回值是引用类型,则返回值
- 返回值是简单类型或者没有返回值,则返回之前创建的对象
其中最需要关注的就是:设置对象 __proto__
属性指向构造函数的原型对象
proto 做了什么?
首先我们需要解释下 __proto__
做了什么?
通常来说,每个 JavaScript 对象都会有 __proto__
这个属性, 这个属性指向当前对象构造函数的原型对象
当代码执行 const value = obj.a
,需要去 obj 对象上寻找 a 这个属性的值,首先会在对象本身的属性中寻找,如果没有找到,则会前往 __proto__
属性所指向的对象上寻找
既然 __proto__
所指向的也是对象,那么这个对象也是会有 __proto__
属性的,那样就会有一直向上回溯的流程
- 如果最后没有找到所需要的属性,则返回
undefined
- 一旦找到,则停止搜索返回结果
差不多的流程是下面这样的:
(我是图)
对应的伪代码:
function find(obj, name) {
for (let key in obj) {
if (key === name) {
return obj[name]
}
}
let temp = obj
while (temp = temp.__proto__) {
for (let key in temp) {
if (key === name) {
return temp[name]
}
}
}
return undefined
}
这样就构成了链式的结构,这大致就是原型链的雏形
(注意:__proto__
不属于语法标准,但是各大浏览器都实现了这个特性,规范的语法是:一个指向 [[Prototype]]
的引用。)
构造函数的原型对象
上面我们了解到:通过 new
关键词创建出来的实例对象会有个 __proto__
属性,这个属性指向构造函数的原型对象。当获取实例对象上的属性的时候,会随着 __proto__
属性的指向回溯搜索
下面我们聊聊对象 __proto__
所指向的 构造函数的原型对象 这个概念
原型对象 prototype
每个函数都会有原型对象:prototype
默认来说,这个对象只会有如下的两个属性:
constructor
指向构造函数__proto__
指向构造函数
constructor
给了我们知道实例对象构造函数的机会,而__proto__
则是用于将原型串联起来
复用
构造函数的原型对象很大意义上解决了复用的问题
一般我们在原型对象上实现一些通用的函数/方法
因为通过 __proto__
属性,每个 new
关键字生成的对象都可以获取对构造函数原型对象的引用
function A() {}
A.prototype.hey = function() { console.log('hey, this is from prototype') }
const a1 = new A()
const a2 = new A()
// 对象 a1 a2 共用相同的 hey 函数
a1.hey === a2.hey // true
保持灵活
而因为 JavaScript
函数中的 this
是运行时决定的值,所以相同的原型对象的函数,会因为执行对象不同,而产生各式的结果
function A(name) {
this.name = name
}
A.prototype.hey = function() { console.log('hey, tis is ' + this.name) }
const tom = new A('Tom')
const jerry = new A('Jerry')
tom.hey() // hey, this is Tom
jerry.hey() // hey, this is Jerry
通过 new
关键词创造出来的所有实例对象,在没有魔改的情况下,它们都保留了 __proto__
属性,即对构造函数原型对象的引用
这样就避免了大量对方法/函数的拷贝工作
注意
不过这样也带来了可以通过实例对象修改原型对象的弊端:
function A(name) {
this.name = name
}
A.prototype.hey = function() { console.log('hey, this is ' + this.name) }
const tom = new A('Tom')
tom.hey() // hey, this is Tom
tom.__proto__.hey = function() { console.log("wow, what's wrong?") }
tom.hey() // wow, what's wrong?'
继承
至此,我们差不多了解了这样的流程:
(我是图)
简单概括来说,只有下面两个概念:
__proto__
属性可以用来回溯搜索对象的属性new
关键字创造出来的实例对象的__proto__
属性指向构造函数的prototype
属性对象
继承的通俗来说就是把已经存在的类所定义的内容作为自己的内容,并加入若干新的内容
这里需要完善
现在我们要做的目标就是将子构造函数的 prototype
对象的 __proto__
属性指向父构造函数的 prototype
对象
简单粗暴
function Father () {}
Father.prototype.say = function() { console.log('This is father') }
function Child () { Father.call(this) }
Child.prototype.__proto__ = Father.prototype
其实这个方案,除了不是标准支持之外,都蛮好的
目前我们知道的可以指定 __proto__
属性的方案,只有使用 new
关键词
借助 new
function Father () {}
Father.prototype.say = function() { console.log('This is father') }
function Child () { Father.call(this) }
Child.prototype = new Father()
Child.prototype.constructor = Child
联系我们上面提到的 new
关键字的作用,prototype
赋值的对象会有__proto__
属性指向父构造函数的原型对象
但是这种方式会将父构造函数的实例属性也赋值到子构造函数的原型对象上,这不是我们预想的结果
规避父构造函数的实例属性
function Father () {}
Father.prototype.say = function() { console.log('This is father') }
function Middle () {}
Middle.prototype = Father.prototype
function Child () { Father.call(this) }
Child.prototype = new Middle()
Child.prototype.constructor = Child
借助空的中间函数,可以避免污染子构造函数的原型对象
标准方案
function Father () {}
Father.prototype.say = function() { console.log('This is father') }
function Child () { Father.call(this) }
Child.prototype = Object.create(Father.prototype)
Child.prototype.constructor = Child
JavaScript
提供的 Object.create()
翻译一下其实就是下面的代码:
Child.prototype = {
__proto__: Father.prototype
}
当然,Object.create()
还支持第二个参数,可以追加其他的属性描述对象