详解 JS 中 new 调用函数原理

摘要:
JavaScript中经常使用构造函数创建对象,那在使用new调用一个函数的时候到底发生了什么?1)看三个例子1.1无return语句构造函数最后没有return语句,这也是使用构造函数时默认情况,最后会返回一个新对象,如下:functionFoo{this.age=age;}varo=newFoo;console.log;这是常见的使用构造函数创建对象的过程,打印出来的是{age:111}。箭头函数中没有[[Construct]]方法,不能使用new调用,会报错。

JavaScript 中经常使用构造函数创建对象(通过 new 操作符调用一个函数),那在使用 new 调用一个函数的时候到底发生了什么?先看几个例子,再解释背后发生了什么。

1)看三个例子

1.1 无 return 语句

构造函数最后没有 return 语句,这也是使用构造函数时默认情况,最后会返回一个新对象,如下:

function Foo(age) {
  this.age = age;
}

var o = new Foo(111);
console.log(o);

这是常见的使用构造函数创建对象的过程,打印出来的是 {age: 111}

1.2 return 对象类型数据

构造函数最后 return 对象类型数据:

function Foo(age) {
  this.age = age;

  return { type: "我是显式返回的" };
}

var o = new Foo(222);
console.log(o);

打印出来的是 {type: '我是显式返回的'},也就是说,return 之前的工作都白做了,最后返回 return 后面的对象。

1.3 return 基本类型数据

那是不是只要构造函数体内最后有 return,返回都是 return 后面的数据呢?

我们看下返回基本类型数据的情况:

function Foo(age) {
  this.age = age;

  return 1;
}

var o = new Foo(333);
console.log(o);

打印出来的是 {age: 333},和没有 return 时效果一样。跟预期不一样,背后你原理看下面分析。

2)背后原理

2.1 非箭头函数的情况

当使用 new 操作符创建对象是,ES5 官方文档在 函数定义 一节中做了如下定义 13.2.2 [[Construct]]

When the [[Construct]] internal method for a Function object F is called with a possibly empty list of arguments, the following steps are taken:

  1. Let obj be a newly created native ECMAScript object.
  2. Set all the internal methods of obj as specified in 8.12.
  3. Set the [[Class]] internal property of obj to Object.
  4. Set the [[Extensible]] internal property of obj to true.
  5. Let proto be the value of calling the [[Get]] internal property of F with argument "prototype".
  6. If Type(proto) is Object, set the [[Prototype]] internal property of obj to proto.
  7. If Type(proto) is not Object, set the [[Prototype]] internal property of obj to the standard built-in Object prototype object as described in 15.2.4.
  8. Let result be the result of calling the [[Call]] internal property of F, providing obj as the this value and providing the argument list passed into [[Construct]] as args.
  9. If Type(result) is Object then return result.
  10. Return obj.

看第 8、9 步:

8)调用函数 F,将其返回值赋给 result;其中,F 执行时的实参为传递给 [[Construct]](即 F 本身) 的参数,F 内部 this 指向 obj
9)如果 resultObject 类型,返回 result

这也就解释了如果构造函数显式返回对象类型,则直接返回这个对象,而不是返回最开始创建的对象。

最后在看第 10 步:

10)如果 F 返回的不是对象类型(第 9 步不成立),则返回创建的对象 obj

如果构造函数没有显式返回对象类型(显式返回基本数据类型或者直接不返回),则返回最开始创建的对象。

2.2 箭头函数的情况

那如果构造函数是箭头函数怎么办?

箭头函数中没有 [[Construct]] 方法,不能使用 new 调用,会报错。

NOTICE:其中 [[Construct]] 就是指构造函数本身。

相关规范在 ES6 的官方文档 中有提,但自从 ES6 以来的官方文档巨难懂,在此不做表述。

3)new 调用函数完整过程

3.1 中文描述及相关代码分析

除了箭头函数之外的任何函数,都可以使用 new 进行调用,背后发生了什么,上节英文讲述的很清楚了,再用中文描述如下:

1)创建 ECMAScript 原生对象 obj
2)给 obj 设置原生对象的内部属性;(和原型属性不同,内部属性表示为 [[PropertyName]],两个方括号包裹属性名,并且属性名大写,比如常见 [[Prototype]][[Constructor]]
3)设置 obj 的内部属性 [[Class]]Object
4)设置 obj 的内部属性 [[Extensible]]true
5)将 proto 的值设置为 Fprototype 属性值;
6)如果 proto 是对象类型,则设置 obj 的内部属性 [[Prototype]] 值为 proto;(进行原型链关联,实现继承的关键
7)如果 proto 是不对象类型,则设置 obj 的内部属性 [[Prototype]] 值为内建构造函数 Objectprototype 值;(函数 prototype 属性可以被改写,如果改成非对象类型,obj[[Prototype]] 就指向 Object 的原型对象)
8)9)10)见上节分析。(决定返回什么)

对于第 7 步的情况,见下面代码:

function Foo(name) {
  this.name = name;
}

var o1 = new Foo("xiaoming");
console.log(o1.__proto__ === Foo.prototype); // true

// 重写构造函数原型属性为非对象类型,实例内部 [[Prototype]] 属性指向 Object 原型对象
// 因为实例是一个对象类型的数据,默认会继承内建对象的原型,
// 如果构造函数的原型不满足形成原型链的要求,那就跳过直接和内建对象原型关联
Foo.prototype = 1;
var o2 = new Foo("xiaohong");
console.log(o2.__proto__ === Foo.prototype); // false
console.log(o2.__proto__ === Object.prototype); // true

3.2 更简洁的语言描述

若执行 new Foo(),过程如下:

1)创建新对象 o
2)给新对象的内部属性赋值,关键是给[[Prototype]]属性赋值,构造原型链(如果构造函数的原型是 Object 类型,则指向构造函数的原型;不然指向 Object 对象的原型);
3)执行函数 Foo,执行过程中内部 this 指向新创建的对象 o
4)如果 Foo 内部显式返回对象类型数据,则,返回该数据,执行结束;不然返回新创建的对象 o

4)几点说明

4.1 判断是否是 Object 类型

关于一个数据是否是 Object 类型,可以通过 instanceof 操作符进行判断:如果 x instanceof Object 返回 true,则 xObject 类型。

由上可知,null instanceof Object 返回 false,所以 null 不是 Object 类型,尽管typeof null 返回 "Object"。

4.2 instanceof 原理

instanceof 的工作原理是:在表达式 x instanceof Foo 中,如果 Foo 的原型(即 Foo.prototype)出现在 x 的原型链中,则返回 true,不然,返回 false

因为函数的原型可以被改写,所以会出现在 x 通过 Foo new 出来之后完全改写 Foo 的原型 x instanceof Foo 返回 false 的情况。因为实例创建之后重写构造函数原型,实例指向的原型已经不是构造函数的新的原型了,见下面代码:

const Foo = function() {};

const o = new Foo();

o instanceof Foo; // true

// 重写 Foo 原型
Foo.prototype = {};
o instanceof Foo; // false

参考资料

What values can a constructor return to avoid returning this?
[[Construct]] internal method

原文地址:https://segmentfault.com/a/1190000015424508

免责声明:文章转载自《详解 JS 中 new 调用函数原理》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇由nginx和spring boot中tomcat配置不当引起的问题npm安装包卡住不动的解决下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

向其他进程注入代码的三种方法

http://huaidan.org/archives/838.html 原版地址: http://www.codeproject.com/threads/winspy.asp?df=100&forumid=16291&select=1025152&msg=1025152 pdf格式下载: http://netxfly.blogbu...

C#编程技巧之钩子函数的使用——SetWindowsHookEx

  本随笔较长,读者可直接到最地下下载示例程序。   总所周知:C#是.NET Framework平台的相伴语言,用它本身的类库和编译器提供的方法是无法实现全局钩子的。但实际上对于非托管代码的调用在C#中是成立的,使用DllImport属性可以引用非托管代码类库中的方法。钩子函数存在于user32.dll中,函数原型如下:   HHOOK WINAPI S...

Module build failed (from ./node_modules/mini-css-extract-plugin/dist/loader.js): ReferenceError: document is not defined

Module build failed (from ./node_modules/mini-css-extract-plugin/dist/loader.js): ReferenceError: document is not defined at insertStyleElement (E:webs est ode_modulesstyle-lo...

安卓进阶:元注解Support Annotation Library使用详解

Support Annotation Library是一个函数包,包含一系列有用的元注解。 注解目录:  Nullness注解  资源类型注解 类型定义注解 线程注解 RGB颜色值注解 值范围注解 权限注解 重写函数注解 返回值注解 @VisibleForTesting @Keep 实用主义至上,不记录历史和版本这些,现在直接了解一下这个函数包有什么用...

Android系统Surface机制的SurfaceFlinger服务对帧缓冲区(Frame Buffer)的管理分析

        在前文中,我们分析了SurfaceFlinger服务的启动过程。SurfaceFlinger服务在启动的过程中,会对系统的硬件帧缓冲区进行初始化。由于系统的硬件帧缓冲区一般只有一个,并且不是谁都可以随便访问的,因此,它就需要由一个服务来统一管理。在Android系统中,这个服务便是SurfaceFlinger。在本文中,我们就详细分析Sur...

python- generator生成器

什么是生成器? 通过列表生成式,我们可以直接创建一个列表,但是,受到内存限制,列表容量肯定是有限的,而且创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。 所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的l...