本文翻译自 https://medium.com/free-code-camp/prototype-in-js-busted-5547ec68872,作者 Pranav Jindal
,翻译时有删改,标题有改动。
以下四行足以使大多数 JavaScript 开发人员感到困惑:
Object instanceof Function
// true
Object instanceof Object
// true
Function instanceof Object
// true
Function instanceof Function
// true
JavaScript 中的原型是极其难以理解的概念之一,但是你不能逃避它。不管你怎么忽略,你终究会在开发过程中碰到原型难题。
所以,让我们直面它吧。
从基础开始,JavaScript 中包含以下数据类型:
- boolean
- number
- string
- undefined
- null
- symbol
- bigint(new)
- object
上面的数据类型中除了对象,其他的都是原始数据类型,他们存储对应类型的数据。
而对象 object
是引用类型,我们可以将其描述为键-值对的集合(事实上不仅如此)。
在 JavaScript 中,可以使用构造函数 (constructor
) 或者对象字面量({}
)创建对象。
JavaScript 中的函数是可以 “调用” 的特殊对象。我们使用 Function
构造函数或者函数声明来创建函数。这些构造函数既是对象又是函数,这个问题始终让我困惑,就像鸡生蛋还是蛋生鸡一样困惑着每个人。
在开始了解原型之前,我想澄清一下 JavaScript 中有两个原型:
-
prototype
:这是一个特殊的对象,它是所有你创建的函数都会有的一个属性。更准确点讲,你创建的任何函数都已经存在该属性,但是这个属性对于 JavaScript 引擎自带的函数或者bind
产生的新函数却是不一定会有的。这个prototype
属性所指向的对象与你用该构造函数创建的对象的[[Prototype]]
属性所指向的对象是同一个; -
[[Prototype]]
:这是每个对象都有的隐藏属性,如果在对象上无法读取到某个属性,则 JavaScript 引擎会尝试从对象的[[Prototype]]
属性指向的对象上继续查找。实例的 这个属性所指向的对象和构造函数的prototype
属性指向的对象是同一个。[[Prototype]]
是给引擎内部使用的,在我们编写的 JS 脚本中可以使用__proto__
属性访问原型对象。还有其他访问此原型的新方法,但是为了简洁起见,我将用__proto__
代替[[Prototype]]
来做讲解;
var obj = {} // 对象字面量
var obj1 = new Object() // 构造函数创建对象
上面两个语句对于创建一个新的对象来讲是一样的,事实上当我们执行上面任何一条语句的时候都发生了很多事情。
当我创建一个新对象的时,创建的是一个空对象。事实上,它并不是空的,因为它是对象构造函数 Object
的一个实例,因此它本身会有一个属性指向 Object.prototype
,而这个属性就是 __proto__
。
如果我们查看 Object
构造函数的 prototype
属性,你会发现它和 obj.__proto__
一模一样。事实上他们是两个不同的指针指向了相同的对象。
obj.__proto__ === Object.prototype
// true
每个函数的 prototype
属性都会有一个 constructor
属性,这个属性都是指向的函数自己。对于 Object
函数,prototype
有一个 constructor
属性指回了 Object
函数。
Object.prototype.constructor === Object
//true
在上面的图片中,左边是 Object
构造函数展开后的。你可能会感到疑惑,里面怎么有这么多函数。函数其实也是对象,因此它也可以像对象一样拥有各种属性。
如果你仔细看,你会发现 Object
(左边的)有一个 __proto__
属性,这意味着 Object
肯定也是由其他有 prototype
的构造函数创建的。由于 Object
是一个函数对象,所以它肯定是由 Function
构造函数创建的。
Object.__proto__
看起来和 Function.prototype
一样。当我检查两者是否全等时,发现它们确实是指向的同一个对象。
Object.__proto__ === Function.prototype
//true
如果你仔细的看上面的图,你也会发现 Function
本身也有一个 __proto__
属性,这意味着 Function
构造函数也一定由其他有 prototype
的构造函数创建而来。由于 Function
本身是一个函数,它肯定是通过 Function
构造函数创建而来,也就是说,它自己创建了自己。这看起来比较荒谬,但是当你检查的时候,它确实是自己创建了自己。
Function
的 __proto__
和 prototype
实际上指向了相同的对象,也就是 Function
的原型对象。
Function.prototype === Function.__proto__
// true
文章前面也说过,函数的 prototype.constructor
属性必然会指向这个函数。
Function.prototype.constructor === Function
// true
上面这张图非常有趣!!
我们再来捋一遍,Function.prototype
也有一个 __proto__
属性。好吧,这也没什么让人惊讶的,毕竟 prototype
是一个对象,它肯定可以有一个这个属性。但是注意,这个属性也是指向 Object.prototype
的。
Function.prototype.__proto__ == Object.prototype
// true
所以有了下面这张图:
// instanceof 操作符
a instanceof b
instanceof
操作符会查找 a
的原型链上的任何 constructor
属性。只要找到了 b
就会返回 true
,否则返回 false
。
现在我们回到文章最开始的四个 instanceof
语句。
Object instanceof Function
Object.__proto__.constructor === Function
Object instanceof Object
Object.__proto__.__proto__.constructor === Object
Function instanceof Function
Function.__proto__.constructor === Function
Function instanceof Object
Function.__proto__.__proto__.constructor === Object
上面的情况太让人纠结了,哈哈!!但是我希望能简单点理解。
这里我有一点没有提出来,那就是 Object.prototype
没有 __proto__
属性。
事实上,它其实有一个 __proto__
属性指向 null
。原型链查找最终会在找到 null
之后停止查找。
Object.prototype.__proto__
// null
Object
, Function
, Object.prototype
和 Function.prototype
也有一些函数属性。如 Object.assign
、Object.prototype.hasOwnProperty
、Function.prototype.call
,这些都是引擎内部函数,他们没有 prototype
属性,它们是 Function
的实例,它们有指向 Function.prototype
的 __proto__
属性。
Object.create.__proto__ === Function.prototype
// true
你也可以探索其他的构造函数,如 Array
、Date
,或者看看它们的实例的 prototype
和 __proto__
。我确定你可以发现这些功能内在的联系。
额外的问题:
这里有几个困扰我一段时间的问题:为什么 Object.prototype
是普通对象而 Function.prototype
是函数对象。
这里 https://stackoverflow.com/a/32929083/1934798 给出了解答。
另一个问题是:原始数据类型是如何调用对应的方法的,如 toString()
、substr()
、toFixed()
?这里 https://javascript.info/native-prototypes#primitives 给出了解释。(译者注:也叫如何理解包装对象)
我把上面两个问题贴到这里。
第一个: 为什么 Function.prototype
是一个函数对象而 Object.prototype
是一个普通对象?
在 ES6 中 Array.prototype
Function.prototype
和其他的构造函数的 prototype
不一样:
-
Function.prototype
是一个 JavaScript 引擎内置的函数对象; -
Array.prototype
是一个引擎内置的数组对象,并且内置有针对这种对象的一些方法。
函数原型对象是为了兼容 ES6 之前的版本的 JS,这也不会让 Function.prototype
成为一个特别的函数。只有构造函数才会有 prototype
属性。
能作为构造函数的函数必须有一个 prototype
属性。
下面有一些非构造函数的例子。
-
Math
对象的方法
typeof Math.pow; // "function
'prototype' in Math.pow; // false
- 一些宿主对象(host objects)
typeof document.createElement('object'); // "function
'prototype' in document.createElement('object'); // false
- ES6 中的箭头函数(正是因为没有
prototype
属性,所以箭头函数不能作为构造函数使用)
typeof (x => x * x); // "function
'prototype' in (x => x * x); // false
第二个:如何理解包装对象
也可以参考这篇文章 https://blog.csdn.net/lhjuejiang/article/details/79623505。
在文章的最开始我们列出了 JS 中的数据类型,其中(这里不考虑 symbol
和 bigint
) boolean
、number
、string
、null
、undefined
都是非引用类型,也就是说变量直接指向的是原始值。
我们平常也会看到下面的操作:
var str = 'hello'; //string 基本类型
var s2 = str.charAt(0);
alert(s2); // h
上面的 string
是一个基本类型,但是它却能召唤出一个 charAt()
的方法,这是什么原因呢?
主要是因为:字符串去调方法的时候,基本类型会找到对应的包装对象类型,然后包装对象把所有的属性和方法给了基本类型,然后包装类型消失。
其过程大概是下面这样:
var str = 'hello'; //string 基本类型
var s2 = str.charAt(0); //在执行到这一句的时候 后台会自动完成以下动作 :
(
var str = new String('hello'); // 1 找到对应的包装对象类型,然后通过包装对象创建出一个和基本类型值相同的对象
var s2 = str.chaAt(0); // 2 然后这个对象就可以调用包装对象下的方法,并且返回结给s2.
str = null; // 3 之后这个临时创建的对象就被销毁了, str =null;
)
alert(s2);// h
alert(str);// hello 注意这是一瞬间的动作 实际上我们没有改变字符串本身的值。
也就是说,当原始值需要用到包装对象的属性或者方法的时候,会构造一个临时的包装对象出来,使用了之后就销毁了。所以即使给这个原始值赋值,由于赋值之后对象会被销毁,之后从这个原始值上并不能获取到对应的属性。
看下面的面试题:把原始值当做一个对象用的时候,所使用的的方法会对隐式产生的包装对象起作用,但不对原始值起作用。
var str="hello";
str.number = 10; // 包装对象消失
alert(str.number); // undefined
最后
往期精彩:
- 前端面试必备 | 使用原型和构造函数创建对象(原型篇:上)
- 前端面试必会 | 一文读懂 JavaScript 中的 this 关键字
- 前端面试必会 | 一文读懂现代 JavaScript 中的变量提升 - let、const 和 var
- 前端面试必会 | 一文读懂 JavaScript 中的闭包
- 前端面试必会 | 一文读懂 JavaScript 中的作用域和作用域链
- 前端面试必会 | 一文读懂 JavaScript 中的执行上下文
- IntersectionObserver 和懒加载
- 初探浏览器渲染原理
- CSS 盒模型、布局和包含块
- 详细解读 CSS 选择器优先级
关注公众号可以看更多哦。
感谢阅读,欢迎关注我的公众号 云影 sky,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。