前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)

前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)_第1张图片

本文翻译自 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 中包含以下数据类型:

  1. boolean

  2. number

  3. string

  4. undefined

  5. null

  6. symbol

  7. bigint(new)

  8. object

上面的数据类型中除了对象,其他的都是原始数据类型,他们存储对应类型的数据。

而对象 object 是引用类型,我们可以将其描述为键-值对的集合(事实上不仅如此)。

在 JavaScript 中,可以使用构造函数 (constructor) 或者对象字面量({})创建对象。

JavaScript 中的函数是可以 “调用” 的特殊对象。我们使用 Function 构造函数或者函数声明来创建函数。这些构造函数既是对象又是函数,这个问题始终让我困惑,就像鸡生蛋还是蛋生鸡一样困惑着每个人。

在开始了解原型之前,我想澄清一下 JavaScript 中有两个原型:

  1. prototype:这是一个特殊的对象,它是所有你创建的函数都会有的一个属性。更准确点讲,你创建的任何函数都已经存在该属性,但是这个属性对于 JavaScript 引擎自带的函数或者 bind 产生的新函数却是不一定会有的。这个 prototype 属性所指向的对象与你用该构造函数创建的对象的 [[Prototype]] 属性所指向的对象是同一个;

  2. [[Prototype]]:这是每个对象都有的隐藏属性,如果在对象上无法读取到某个属性,则 JavaScript 引擎会尝试从对象的 [[Prototype]] 属性指向的对象上继续查找。实例的 这个属性所指向的对象和构造函数的 prototype 属性指向的对象是同一个。[[Prototype]] 是给引擎内部使用的,在我们编写的 JS 脚本中可以使用 __proto__ 属性访问原型对象。还有其他访问此原型的新方法,但是为了简洁起见,我将用 __proto__ 代替 [[Prototype]] 来做讲解;

var obj = {} // 对象字面量
var obj1 = new Object() // 构造函数创建对象

上面两个语句对于创建一个新的对象来讲是一样的,事实上当我们执行上面任何一条语句的时候都发生了很多事情。

当我创建一个新对象的时,创建的是一个空对象。事实上,它并不是空的,因为它是对象构造函数 Object 的一个实例,因此它本身会有一个属性指向 Object.prototype ,而这个属性就是 __proto__

前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)_第2张图片

如果我们查看 Object 构造函数的 prototype 属性,你会发现它和 obj.__proto__ 一模一样。事实上他们是两个不同的指针指向了相同的对象。

前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)_第3张图片
obj.__proto__ === Object.prototype
// true

每个函数的 prototype 属性都会有一个 constructor 属性,这个属性都是指向的函数自己。对于 Object 函数,prototype 有一个 constructor 属性指回了 Object 函数。

Object.prototype.constructor === Object
//true
前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)_第4张图片

在上面的图片中,左边是 Object 构造函数展开后的。你可能会感到疑惑,里面怎么有这么多函数。函数其实也是对象,因此它也可以像对象一样拥有各种属性

如果你仔细看,你会发现 Object (左边的)有一个 __proto__ 属性,这意味着 Object 肯定也是由其他有 prototype 的构造函数创建的。由于 Object 是一个函数对象,所以它肯定是由 Function 构造函数创建的。

前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)_第5张图片

Object.__proto__  看起来和 Function.prototype 一样。当我检查两者是否全等时,发现它们确实是指向的同一个对象。

Object.__proto__ === Function.prototype
//true

如果你仔细的看上面的图,你也会发现 Function 本身也有一个 __proto__ 属性,这意味着 Function 构造函数也一定由其他有 prototype 的构造函数创建而来。由于 Function 本身是一个函数,它肯定是通过 Function 构造函数创建而来,也就是说,它自己创建了自己。这看起来比较荒谬,但是当你检查的时候,它确实是自己创建了自己。

前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)_第6张图片

Function__proto__prototype 实际上指向了相同的对象,也就是 Function 的原型对象。

Function.prototype === Function.__proto__
// true

文章前面也说过,函数的 prototype.constructor 属性必然会指向这个函数。

Function.prototype.constructor === Function
// true
前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)_第7张图片

上面这张图非常有趣!!

我们再来捋一遍,Function.prototype 也有一个 __proto__ 属性。好吧,这也没什么让人惊讶的,毕竟 prototype 是一个对象,它肯定可以有一个这个属性。但是注意,这个属性也是指向 Object.prototype 的。

Function.prototype.__proto__ == Object.prototype
// true

所以有了下面这张图:

前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)_第8张图片
// 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.prototypeFunction.prototype 也有一些函数属性。如 Object.assignObject.prototype.hasOwnPropertyFunction.prototype.call,这些都是引擎内部函数,他们没有 prototype 属性,它们是 Function 的实例,它们有指向 Function.prototype__proto__ 属性。

前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)_第9张图片
Object.create.__proto__ === Function.prototype
// true

你也可以探索其他的构造函数,如 ArrayDate,或者看看它们的实例的 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 是一个普通对象?

前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)_第10张图片

在 ES6 中 Array.prototype Function.prototype 和其他的构造函数的 prototype 不一样:

  1. Function.prototype 是一个 JavaScript 引擎内置的函数对象;

  2. Array.prototype 是一个引擎内置的数组对象,并且内置有针对这种对象的一些方法。

函数原型对象是为了兼容 ES6 之前的版本的 JS,这也不会让 Function.prototype 成为一个特别的函数。只有构造函数才会有 prototype 属性。

能作为构造函数的函数必须有一个 prototype 属性。

下面有一些非构造函数的例子。

  1. Math 对象的方法

typeof Math.pow; // "function
'prototype' in Math.pow; // false
  1. 一些宿主对象(host objects)

typeof document.createElement('object'); // "function
'prototype' in document.createElement('object'); // false
  1. ES6 中的箭头函数(正是因为没有 prototype 属性,所以箭头函数不能作为构造函数使用

typeof (x => x * x); // "function
'prototype' in (x => x * x); // false

第二个:如何理解包装对象也可以参考这篇文章 https://blog.csdn.net/lhjuejiang/article/details/79623505。

在文章的最开始我们列出了 JS 中的数据类型,其中(这里不考虑 symbolbigintbooleannumberstringnullundefined 都是非引用类型,也就是说变量直接指向的是原始值。

我们平常也会看到下面的操作:

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 中的执行上下文

  • InterpObserver 和懒加载

  • 初探浏览器渲染原理

  • CSS 盒模型、布局和包含块

  • 详细解读 CSS 选择器优先级

关注公众号可以看更多哦。

感谢阅读,欢迎关注我的公众号 云影 sky,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。

前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)_第11张图片 公众号 前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中)_第12张图片 交流群

你可能感兴趣的:(前端面试必备 | 古怪的原型(鸡生蛋还是蛋生鸡)(原型篇:中))