ECMAScript 函数对象和 new你到底做了什么

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇

前戏

问:new 后面跟着的是什么呢?

答:是函数对象ECMAScript Function Objects

这不还有 class 嘛?

javascript 复制代码
typeof class {}  // 'function'

从本质上讲,class 是function 的一种语法糖,简化了编程的语法而已。

本章节和本册都不会深入 class, class 是下册的内容。

函数对象 (简单了解)

class 类本质也是函数对象,所以下面的会出现很多与class相关的字段。

协议对应的内容在 Table 30: Internal Slots of ECMAScript Function Objects

不用记,有个印象即可。

Internal Slot Type Description
[[Environment]] 环境记录 Environment Record 函数创建时关联的环境记录
[[PrivateEnvironment]] Environment Record 或者 null 如果此函数在语法上不包含在class中,则为null。当评估函数代码时,用作内部类的外部私有环境。何为内部类,表格下方有示例。
[[FormalParameters]] 解析节点 Parse Node 形参列表
[[ECMAScriptCode]] 解析节点 Parse Node 函数对象对应的解析节点
[[ConstructorKind]] BASE or DERIVED DERIVED为派生类构造函数,会进行super调用。
[[Realm]] 领域记录 Realm Record 提供内在对象
[[ScriptOrModule]] 脚本记录Script Record 或者 模块记录Module Record 函数对象换脸的模块脚本或者普通脚本。
[[ThisMode]] LEXICAL, STRICT, or GLOBAL 定义了函数中this引用在形式参数和函数体内部的解释规则。LEXICAL:箭头函数STRICT:严格模式Global:非严格模式下,如果函数内的this值为undefined或null
[[Strict]] a Boolean 如果这是一个严格函数,则为 true; 如果这是一个非严格函数,则为 false。
[[HomeObject]] an Object 如果函数使用了super关键字,这个对象的[[GetPrototypeOf]]内部方法会提供一个对象,该对象作为super属性查找的起始点
[[SourceText]] 一串Unicode码点序列 函数对应的原文本
[[Fields]] ClassFieldDefinition Records 列表 如果该函数是一个类(class),则这部分是一个记录列表,用于表示类的非静态字段(成员变量)及其对应的初始化器(初始化表达式)
[[PrivateMethods]] PrivateElements 列表 如果该函数是一个类(class),这部分则是一个列表,代表了类的非静态私有方法和访问器。
[[ClassFieldInitializerName]] a String, a Symbol, a Private Name, or EMPTY 当函数是用来初始化类中某个字段的,默认情况下,会提供一个名称来标识该字段以进行特定的初始化过程;如果不是用于字段初始化,则此字段留空。
[[IsClassConstructor]] a Boolean 指示函数是否为类构造函数。(如果为真,调用函数的[[ Call ]]将立即抛出 TypeError 异常。)

内部类示例:

javascript 复制代码
class OuterClass {   
    constructor() {     
        // 内部类     
        class InnerClass {       
            innerMethod() {         
                console.log("Inner method");      
            }     
        }     
        this.innerInstance = new InnerClass();  
    }    
    
    outerMethod() {     
        this.innerInstance.innerMethod();   
    } 
}

[[IsClassConstructor]]来识别是普通的函数还是class,而[[ConstructorKind]]是来识别是不是派生类。

回顾一下 函数对象 Function Objects 另外两个特别的内置方法

一个 [[Call]]管调用, 一个是 [[Construct]]管构造,即可以被new。

什么能 new

答案:有内部方法[[Construct]]的函数。

协议内部就是通过 IsConstructor 检查 是不是有 [[Construct]]内部方法。

非函数对象自然没有这个内部方法,当然也不是所有函数对象都有这个方法。

那函数对象的 [[Construct]] 这个内部方式又是什么时候被设置上的呢?当然是之前章节函数实例化 InstantiateFunctionObject 的时候 。

InstantiateFunctionObject 这个抽象操作把 函数对应的解析节点转为 函数对象Function Object, 根据函数类型不同,解析逻辑也是不同的

普通函数可以new

普通函数实例化对应协议内容 InstantiateOrdinaryFunctionObject

普通函数实例化核心三步

  • 创建普通函数对象
  • 设置函数名
  • 制作构造函数:设置 [[Construct]]属性和原型

制作构造函数是通过 MakeConstructor来实现的。

MakeConstructor 这个过程除了设置 [[Construct]],此外脚本产生的函数,还设置了 原型prototype, 以及其constructor属性。

生成器函数,异步函数和异步生成器函数

这三种函数都是不可以被new的,实例化的过程 都没有 MakeConstructor 的过程

注意, 异步函数自身是没有设置过 原型(prototype)的

javascript 复制代码
async function asyncFun(){}
Object.getOwnPropertyDescriptor(asyncFun, "prototype") // undefined

function *genFun(){}
Object.getOwnPropertyDescriptor(genFun, "prototype") // {value: Generator, writable: true, enumerable: false, configurable: false}

async function  asyncGenFun(){}
Object.getOwnPropertyDescriptor(genFun, "prototype") // {value: Generator, writable: true, enumerable: false, configurable: false}

这三种函数没有 [[Construct]] 内部字段,自然不能被 new

javascript 复制代码
async function asyncFun(){}
new asyncFun();       // Uncaught TypeError: asyncFun is not a constructor

function *genFun(){}
new genFun()         // Uncaught TypeError: genFun is not a constructor

async function  asyncGenFun(){}
new asyncGenFun();   // Uncaught TypeError: asyncGenFun is not a constructor

箭头函数 和 异步箭头函数

箭头函数部分对应协议 15.3 Arrow Function Definitions, 实例化的逻辑位于 15.3.4 Runtime Semantics: InstantiateArrowFunctionExpression

其也没有设置 [[Construct]], 所以呢,你懂的

异步箭头函数是类似的

属性方法

class 原型方法定义和对象的方法定义 对应协议内容 15章ECMAScript Language: Functions and Classes 的 第四节 15.4 Method Definitions,对应的解析节点是 MethodDefinition

普通方法属性

普通方法底又都走的是 DefineMethod,其函数实例化过程并未设置 [[Construct]]。

对象字面量初始化时,属性定义的逻辑是包含 MethodDefinition

一起从解析节点来看看 var a = { funA(){} } 这行代码对应的解析节点大致如下:

同样的 class classA { method(){} }对应的解析节点大致如下:

从上面的解析节点也可以知道,对象上的普通方法属性和class上的普通方法属性 的节点类型都是 MethodDefinition, 只不过 ClassElementName , FunctionBody 以及 UniqueFormalParameters 不同。

异步方法,迭代器方法和异步迭代器方法属性 也是类似的情况。

那么再通过一段代码升华一下:

javascript 复制代码
var obj = {
  objFun(){},
}
obj.AssignmentFun = function (){};

new obj.AssignmentFun(); // obj.AssignmentFun {}
new obj.objFun();        // Uncaught TypeError: obj.objFun is not a constructor

你会发现,同样是对象属性,后赋值而产生的方法属性,是可以被new的,随着字面量产生的却是不可以。

至于为什么,大脑 + 小脑一起想一想。

异步方法,迭代器方法和异步迭代器方法属性

这三种方法(函数)和不作为属性时表现一致,三种方法属性都未设置 [[Construct]],所以都不能被new

class 属性:

javascript 复制代码
class classA {

    *generatorMethod() {}

    async asyncMethod() {}

    async*asyncGeneratorMethod() {}
}

const instanceA = new classA();

// Uncaught TypeError: instanceA.generatorMethod is not a constructor
new instanceA.generatorMethod();
//

// TypeError: instanceA.asyncMethod is not a constructor
new instanceA.asyncMethod();

// Uncaught TypeError: instanceA.asyncGeneratorMethod is not a constructor
new instanceA.asyncGeneratorMethod();

对象属性:

javascript 复制代码
const objA = {
    *generatorMethod() {},
    async asyncMethod() {},
    async*asyncGeneratorMethod() {},
}

// Uncaught TypeError: objA.generatorMethod is not a constructor
new objA.generatorMethod();
//

// TypeError: objA.asyncMethod is not a constructor
new objA.asyncMethod();

// Uncaught TypeError: objA.asyncGeneratorMethod is not a constructor
new objA.asyncGeneratorMethod();

下面就是各自的对应的协议部分:

迭代器方法属性

异步迭代器方法属性

异步方法属性

箭头函数 和 异步箭头函数 属性方法

不作为属性时表现一致,方法属性都未设置 [[Construct]],所以都不能被new

javascript 复制代码
var obj = {
  arrowFun: () => {},
  arrowAsycFun: async ()=> {}
}

new obj.arrowFun();     // Uncaught TypeError: obj.arrowFun is not a constructor
new obj.arrowAsycFun(); // Uncaught TypeError: obj.arrowAsycFun is not a constructor

class 相关的函数

class 自然是能new的,细节在专题会讨论。

需要注意的点

  • class的静态方法是不可以被new的
javascript 复制代码
class classA {
  constructor(){
    
  }

  static staticMethod(){
    
  }

  get getFun(){

    return function fun(){}
  }

  arrowFun = ()=> {
    
  }

  commonMethod(){
    
  }
  
}


new ClassA.staticMethod(); // VM92:1 Uncaught TypeError: classA.staticMethod is not a constructor

const ins = new classA();

new ins.constructor();  // classA {arrowFun: ƒ}

new ins.arrowFun();    // Uncaught TypeError: ins.arrowFun is not a constructor

new ins.getFun        // fun {}

new 的 逻辑

new 操作的对应协议 The new Operator

EvaluateNew ( constructExpr, arguments )

调用链路:

前面的逻辑基本都是做检查和参数处理,核心还是 函数对象的 [[Construct]]

[[Construct]]的逻辑

对应者协议的 10.2.2 [[Construct]] ( argumentsList, newTarget ), 重点看红色部分。

  1. 非派生类的情况下,会新建一个对象作为 this 的值(派生类是由父类创建的)
  2. this 的值是执行时确定的,会尝试在词法环境记录上去绑定this的值,也是有例外的,如果是箭头函数,那么不会进行绑定,当然箭头函数是不可以被new的。
  3. 构造函数会被执行,为返回的对象添加属性以及其他操作
  4. 返回值大有讲究
    1. 如果有return语句
      1. 返回的是对象,直接返回
      2. 如果不是对象且不是派生类,返回 this
      3. 如果返回的不是 undefined , 抛出TypeError
    2. 不是return语句
      1. 如果this 不是 对象, 抛出异常
      2. 返回 this

此时,再回顾一下,网传的new做了什么

  1. 创建一个新对象
  2. 设置原型
  3. 设置this,执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象

基本大差不差,稍微提一下

  1. 如果是衍生类,this 是在 父类中创建的
  2. 返回的不一定是内部创建的新对象

返回值

为了更好的理解返回值,必须进一步了解 函数对象内部字段 [[ConstructorKind]], 也是逻辑描述中kind。

Table 30: Internal Slots of ECMAScript Function Objects

Internal Slot Type Description
[[ConstructorKind]] BASE or DERIVED DERIVED为派生类构造函数,会进行super调用。

因为普通函数和非衍生类,值是BASE, OK, 知道这么多就够了, 一起来练习练习。

示例1

javascript 复制代码
function A(){
  this.a = 'a'
  return undefined
}

new A()

提示:

示例2

javascript 复制代码
function A(){
  this.a = 'a'
}

new A()

提示:

示例3

javascript 复制代码
class Super {
  constructor(){}
}

class Suber extends Super{
  constructor(){
    super()
    this.a = 'a'
    return undefined
  }  
}

new Suber()

提示:

示例4

javascript 复制代码
class Super {
  constructor(){}
}

class Suber extends Super{
  constructor(){
    super()
    this.a = 'a'
    return 1
  }  
}

new Suber()

提示:

小结

  1. 有内部方法[[Construct]]的函数 可以被 new (class 是 function 的一种语法糖)
  2. 函数的[[Construct]]方法是 实例化时被创建的
    1. 只有普通方法有 [[Construct]]
    2. 不是所有普通方法都有 [[Construct]]
  3. 网传四步曲 大致描述了整个 new 的过程
    1. 创建一个新对象
    2. 设置原型
    3. 设置this,执行构造函数中的代码(为这个新对象添加属性)
    4. 返回新对象
  4. return 的值大有玄机

引用

The new Operator

相关推荐
高木的小天才8 分钟前
鸿蒙中的并发线程间通信、线程间通信对象
前端·华为·typescript·harmonyos
Danta1 小时前
百度网盘一面值得look:我有点难受🤧🤧
前端·javascript·面试
OpenTiny社区1 小时前
TinyVue v3.22.0 正式发布:深色模式上线!集成 UnoCSS 图标库!TypeScript 类型支持全面升级!
前端·vue.js·开源
dwqqw1 小时前
opencv图像库编程
前端·webpack·node.js
Captaincc2 小时前
为什么MCP火爆技术圈,普通用户却感觉不到?
前端·ai编程
海上彼尚3 小时前
使用Autocannon.js进行HTTP压测
开发语言·javascript·http
阿虎儿3 小时前
MCP
前端
layman05283 小时前
node.js 实战——(fs模块 知识点学习)
javascript·node.js
毕小宝3 小时前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML3 小时前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能