js 从语言设计的角度问:为什么要设计原型prototype ?

js从语言设计的角度问:为什么要设计原型prototype

设计原型prototype的初心?

从一个语言设计者的角度想一下,为什么要有原型? javascript 设计出原型prototype,想要解决什么问题呢?没有无缘无故的爱,也没有没有无缘无故的恨。不会觉得好玩,凭空产生的。对不!

Brendan Eich (JavaScript 之父)花了10天的时间设计出来,然后出于营销推广的目的,当时java 名气比较大,以java为前缀,伟大的javascript 诞生了。从此java 和 javascript 成了一辈子的好兄弟。哈哈。。。任何一门编程语言,都要解决一个问题:共享/复用,重要的事情说3遍,(共享/复用) x 3,同一个业务逻辑,同一个状态、行为javascript 你打算让我copy 3次吗?当然是不要。那么是借鉴其它语言的类,模块,包来解决吗?可能经过作者的权衡,天才的构思,原型prototype,就这样应运而生了

原型构成的2个关键角色?

从语言设计的角度看,[[Prototype]]或者 proto 用来解决什么呢?prototype 是什么,主要是用来干嘛?这个得打电话给 Brendan Eich (JavaScript 之父),好好聊一下了。啥?,,,不记得号码了。好吧,我们从实现上去理解:

  • [[Prototype]]或者 proto:是相对实例而言的,指向实例的原型,负责处理的是指向关系
  • prototype:是相对构造函数而言,构造函数上的一个属性,负责处理的是共享/复用。 构造函数的prototype 称作 原型对象

实例的原型是什么?构造函数的原型对象是什么?以这样去提问,对理解是有帮助的


// 指定共享的属性/方法
const customPrototype = { id: 0, refer: true, action: function(){} }

function Ancestor(){}

// 指定构造函数的原型对象
Ancestor.prototype = customPrototype

const a = new Ancestor()
const b = new Ancestor()

// 看一下效果
Object.getPrototypeOf(a) === a.__proto__  // true

Object.getPrototypeOf(a) === customPrototype // true

a.id === 0 // true

a.id === b.id // true

b.action === a.action // true

复制代码

为了降低理解的成本,先说明一下: Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值。)像[[Prototype]]这种格式,带2个中括号包围的,说明它是浏览器的内部属性。

分析一下代码

  • a.id === 0; a.id === b.id:属性为0,共享同一属性
  • b.action === a.action: 指向了同一个函数,共享同一方法
  • Object.getPrototypeOf(a) === customPrototype:实例a的原型 customPrototype,等价于构造函数的原型对象

谈到原型就要有一个相对的概念,要明确[[Prototype]]或者 proto 的是处理关系。构造函数的prototype 处理共享/复用

指定一个原型哪些方式?


// 方式一: 设置构造函数的 prototype 
const customPrototype = { id: 0, refer: true }

function Ancestor(){}

Ancestor.prototype = customPrototype

const a = new Ancestor()

// 方式二:Object.create
const b = Object.create(customPrototype)

// 方式三:setPrototypeOf, 会引发性能问题,有兴趣的可点击文末的引用连接
const c = Object.setPrototypeOf({},customPrototype)

// 方式四:重新赋值 __prototype
const d = {}
d.__proto__ = customPrototype

// 看一下效果
Object.getPrototypeOf(a) === Object.getPrototypeOf(b) // true
Object.getPrototypeOf(b) === Object.getPrototypeOf(c) // true
Object.getPrototypeOf(c) === Object.getPrototypeOf(d) // true

复制代码

有一点要注意:proto 不是EMCA规范,是浏览器作为宿主环境实现的非标准属性,IE 上不一定都提供。[[Prototype]], 在浏览器上不支持访问, 代码只能使用 proto 进行赋值操作,为了方便理解 proto 和 [[Prototype]], 可以理解成等价,不作过多的区分。

运行时改变原型会怎么样?

const customPrototype = { id: 0, refer: true }
const anothePrototype = { id: 1, refer: false }

function Ancestor(){}

Ancestor.prototype = customPrototype

const a = new Ancestor()

// 运行时改变构造函数原型
Ancestor.prototype = anothePrototype

const b = new Ancestor()

Object.getPrototypeOf(a) === Object.getPrototypeOf(b) // false
Object.getPrototypeOf(a) === customPrototype // true
Object.getPrototypeOf(b) === anothePrototype // true

// 再改变一次
Object.setPrototypeOf(b,customPrototype)
Object.getPrototypeOf(a) === Object.getPrototypeOf(b) // true

复制代码

从上面的代码可以得出:原型是可以在运行时重新指定的,b 实例也好,构造函数Ancestor也好,都可以动态指定。
只是在 new Ancestor() 调用了,使用 customPrototype 作为 b 实例的原型。

Object.setPrototypeOf 改变的是[[Prototype]]的值,Ancestor.prototype = anothePrototype 改变的是
构造函数的被使用 new 操作调用时,生成的实例链接的对象变成 anothePrototype。

原型链是怎么产生的?

实例的原型是通过[[Prototype]]进行链接,可以理解成一个链表,一个节点指向上一个节点,直到尽头。前面我们已经知道实例的原型是可以在运行时改变的,那么原型链也是动态的。


const customPrototype = { level: 0 }

function Ancestor(){}

Ancestor.prototype = customPrototype

const a = new Ancestor()

function A1() {}

A1.prototype = a

a1 = new A1()

// 测试一下指向哪个原型
a1.__proto__ === a // true
a1.__proto__.__proto__ === customPrototype // true

// 改变实例指向的原型
Object.setPrototypeOf(a1,customPrototype)

a1.__proto__ === a // false
a1.__proto__ === customPrototype // true

// 遍历整个原型链
function prototypeChain(instance) {
  // 没有原型可言
  if(instance === void 0 || instance === null) {
    return 
  } 

  let prototype = Object.getPrototypeOf(instance)
  do {
    console.log(prototype)
    prototype = Object.getPrototypeOf(prototype)
  } while(prototype)
  console.log(prototype)
}

prototypeChain(a1)

复制代码

原型的生产流水线?

new 调用构造函数发生了什么?constructor 设计的初衷是什么?了解设语言设计api的初衷,可以看buildint,初始化是怎么样的?

constructor 在哪里,主要处理什么事情?


function Ancestor(){}

const obj = new Ancestor()

// 构造函数对象prototype 的一个属性
Ancestor.prototype.constructor === obj.constructor // true
Ancestor.prototype.constructor === Ancestor // true
Object.getOwnPropertyDescriptor(obj,'constructor') === undefined // true

复制代码

从上面的3个等式,可以看出

  • constructor:是构造函数原型对象prototype 的一个属性
  • constructor:指向构造函数自身
  • 实例上的constructor,是通过继承得来的,访问的是构造函数原型对象prototype.constructor

constructor 本质是用来说明实例是调用哪个构造函数生成

在constructor哪里清楚啦,我们来看一下,它主要处理什么事情?


function Ancestor(){
  console.log('Ancestor')
}
function Descendant(){
  console.log('Descendant')
}

Ancestor.prototype = { id: 0 }
Descendant.prototype = { id: 1 }

Ancestor.prototype.constructor = Descendant

const obj = new Ancestor()

obj.constructor === Descendant // true
obj.id === 1 // false
obj.id === 0 // true

复制代码
  • Descendant 没有调用:说明原型对象 Ancestor.prototype.constructor 只是用来标记
  • obj.id === 0: 说明实例的原型是 Ancestor.prototype
  • obj.constructor === Descendant: 综上2点,可以得出 constructor 来自实例原型上的constructor

从目前来看 constructor 只实例原型上的为作构造函数标记,用来说明实例的类型,再来验证一下:

function Ancestor(){}

Ancestor.prototype.constructor = 'mark type'

const obj = new Ancestor()

obj.constructor === 'mark type' // true

复制代码

到此,可以明确constructor 可以作为一个字符串,不是一定要作为函数。主要是标记实例的类型,是被哪个构造函数生成的。当然注意的是可以随意改变的,那么用 constructor 并不准确的。

new 发生了什么?

new 以这种方式调用构造函数会执行如下操作:

(1)在内存中创建一个新对象。
(2)这个新对象内部的[[Prototype]]特性被赋值为构造函数的prototype属性。
(3)构造函数内部的this被赋值为这个新对象(即this指向新对象)。
(4)执行构造函数内部的代码(给新对象添加属性)。
(5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

这个流程是一定这样的吗?会受到其它其它因素的影响吗?带着这些疑问,我们用代码的求证。


function Ancestor(){}
function Descendant() {
  // 情况一:返回一个对象
  return { id: 1 }
}

// 情况二:原型对象是基本数据类型:string, number, undefined, null
Ancestor.prototype = null

Descendant.prototype = { id: 0 }

const a = new Ancestor()
const b = new Descendant()

// 测试等式
Object.getPrototypeOf(a) === null // false
Object.getPrototypeOf(a) === Object.prototype // true
b.id === 0 // false
b.id === 1 // true

复制代码

在用new 操作符调用构造函数的过程中,有2个细节要注意

  • 第(2)步的 [[Prototype]], 会判断原型对象数据类型,如果是基本的数据类型(string,number, null, undefined, 布尔值,Symbol), 那么原型对象不会被链接。 [[Prototype]] 赋值为 Object.prototype

  • 构造函数的return 返回值也是要分情况,如果是返回的是引用数据类型,第(3)步的this被丢弃,返回值作为实例值。
    没有返回值或者返回的是基本数据类型,则返回的是 this(也就是新对象)

如何检测原型关系?

原型链可以理解成一个链表,进行搜索任务时,是一个节点一个节点递归的向上查找:1.找到并返回 2.到达最后一个节点,没有找到退出。

要判断一个实例是不是某一个构造函数生成的,可以使用instanceof,该运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

function Ancestor(){}

let a = new Ancestor()


// 等式一
a instanceof Ancestor === true // true
// 等式二为真:因为实例a原型链找到了构造函数Ancestor的原型对象
Object.getPrototypeOf(a) === Ancestor.prototype // true

Ancestor.prototype = new Array()

a = new Ancestor()

a instanceof Ancestor // true

// 等式二
a instanceof Array // true
// 等式二为真:因为实例a原型链找到了构造函数Array的原型对象
a.__proto__.__proto__ === Array.prototype // true

function Descendant() {
  return { id: 1 }
}

const b = new Descendant()

// 等式不成立
// 构造函数 Descendant 返回 { id: 1 }, 这样 Descendant.prototype原型对象,没有进行链接
b instanceof Descendant // false


// instanceof 操作符限制

// 情况一:包装对象
Object.getPrototypeOf(3) === Number.prototype // true
3 instanceof Number // false

// 情况二:instanceof 和多全局对象(例如:多个 frame 或多个 window 之间的交互)

// 情况三:当然还会有一些情况,可以从instanceof 判断的依据去想想

复制代码

内置对象原型关系图?

浏览器会内置哪些对象?Object 是比较高频出现的,Array,Date 也可能经常用到。Function 可能并没有经常出现,但是在框架可以经常看到他的踪迹。Function 才是内置对象的核心枢纽,那我们一起看看Function 有什么特别?


// 特别之处一:Function 的原型对象是一个 ƒ () { [native code] }
// 对的,它是一个函数, 就这么特别
typeof Function.prototype === 'function' // true
// Object的原型对象是一个 对象
typeof Object.prototype === 'object' // true

// 特别之处二:Function 作为一个实例来看,那的原型是什么?
// 对的,Function 的原型是它自身的原型对象, 也就是 Function.prototype
Object.getPrototypeOf(Function) === Function.prototype // true

复制代码

看起来很特别了,那么我们再看看Function.prototype 到底是什么?


// Function.prototype 的 [[Prototype]] 是 Object.prototype
Object.getPrototypeOf(Function.prototype) === Object.prototype // true
// 和其它构造函数一样,指向自身
Function.prototype.constructor === Function // true

复制代码

从上面的可以知道 Function.prototype 的原型是 Object.prototype,但是,还是没有找到更加明确的信息。这个时候可以查看规范,官方链接

The Function prototype object is itself a Function object (its [[Class]] is “Function”) that, when invoked, accepts any arguments and returns undefined.

The value of the [[Prototype]] internal property of the Function prototype object is the standard built-in Object prototype object (15.2.4). The initial value of the [[Extensible]] internal property of the Function prototype object is true.

The Function prototype object does not have a valueOf property of its own; however, it inherits the valueOf property from the Object prototype Object.

The length property of the Function prototype object is 0.

翻译一下:Function 原型对象本身就是一个 函数对象(它的 [[Class]] 是“Function”,是可调用的),当被调用时,它接受任意参数并返回 undefined。

Function 原型对象的 [[Prototype]] 内部属性的值是标准的内置 Object.prototype, Function 原型对象的 [[Extensible]] 内部属性的初始值为 true。

Function 原型对象没有自己的valueOf 属性;然而它从 Object.prototype 继承了 valueOf属性。需要注意的是valueOf 是一个函数,翻译为 valueOf 方法更贴切。

Function 原型对象的长度属性为 0,也就说 Function.prototype 作为函数调用,他们参数个数是0;

综合上面的我们做一个总结:

  • Function.prototype: Function 的原型对象是一个函数;作为函数调用,接受任意参数,并返回 undefined
  • Function 的原型:是指向 Function.prototype,这也是原型链 关系出现的关键,就是常说的
  • Function 的原型对象的原型: Function.proto.proto === Object.prototype 就这样,Function 和 Object 产生了关系

Function 和 Object 的环形关系

// Function 的原型是 Function 的原型对象这个是环产生的原因
Object.getPrototypeOf(Function) === Function.prototype // true
// Object 的原型是Function 原型对象
Object.getPrototypeOf(Object) === Function.prototype // true
Object.getPrototypeOf(Function.prototype) === Object.prototype // true
Object.getPrototypeOf(Object.prototype) === null // true


复制代码

从实例来看,我们讲原型是什么?从构造函数来看,我们讲的是原型对象,这样就不会出现绕的情况。从语言设计的角度,他们出现都是为了解决一些问题,回顾前面讲的。我们再来梳理一下,上面的为什么要这样设计

  • Function.prototype: 更多的是作为函数可以被调用,作为所有函数的鼻祖,最初始的状态,prototype 本身就有雏形的意思。

  • Object.prototype: 更多的是作为对象的原始公用属性和方法提供者,本身承载就是共享/复用职责

  • null:是一切原型的雏形,最纯粹,没有任何方法,属性。

  • Object 是一个函数,为获得可调用这个特征,把[[Prototype]] 指向 Function.prototype 很好理解

  • Function 是一个可调用的对象,注意是对象,原型链必须有一节点,把[[Prototype]] 指向 Object.prototype

  • 如果 Function.proto 指向 null 会怎么样?那 Function 就没有办法拥有 对象的表现了,不能称为是一个可调用对象了。因为要是函数,又要是对象,一开始 Object.prototype, Function.prototype 是分开的,要达到这样的一个效果,就要有一个交叉点。这样Function.proto 直接指向身原型,这个方案可行,也挺完美的。当然也可以增加一个节点,再指向Object.prototype 也是可以,但这样很啰嗦了。

曾经也想过一个问题:先有Function.prototype 还是先Object.prototype。其实这个问题是不存在的,从上面来看。先有哪个都是可以的,通过[[Prototype]]链接起来就可以。先有什么,后有什么,本身就有时间的这个设定条件,时间24h本身就是人类创造出来的,是一个虚拟的概念。就好比先有鸡,还是先有蛋。鸡和蛋,本质上就是物质,和时间没有关系。试想生物技术足够发达,直接分别生产出鸡和蛋,你还会问先有哪个吗?

结束语


以上是个人对原型的一些理解,如有错漏,欢迎指出纠正。从语言的设计的角度去看,更多从api 的设计的初衷,使用的角度去看,希望不要造成误解,仅仅是个人的一些猜测。

参与资料

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享