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 的设计的初衷,使用的角度去看,希望不要造成误解,仅仅是个人的一些猜测。