TomIsion/blog-backup

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() 还支持第二个参数,可以追加其他的属性描述对象

参考资料