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

相关推荐
麻芝汤圆23 分钟前
MapReduce 入门实战:WordCount 程序
大数据·前端·javascript·ajax·spark·mapreduce
juruiyuan1112 小时前
FFmpeg3.4 libavcodec协议框架增加新的decode协议
前端
Peter 谭3 小时前
React Hooks 实现原理深度解析:从基础到源码级理解
前端·javascript·react.js·前端框架·ecmascript
周胡杰3 小时前
鸿蒙接入flutter环境变量配置windows-命令行或者手动配置-到项目的创建-运行demo项目
javascript·windows·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
LuckyLay4 小时前
React百日学习计划——Deepseek版
前端·学习·react.js
gxn_mmf4 小时前
典籍知识问答重新生成和消息修改Bug修改
前端·bug
hj10434 小时前
【fastadmin开发实战】在前端页面中使用bootstraptable以及表格中实现文件上传
前端
乌夷4 小时前
axios结合AbortController取消文件上传
开发语言·前端·javascript
晓晓莺歌5 小时前
图片的require问题
前端
码农黛兮_465 小时前
CSS3 基础知识、原理及与CSS的区别
前端·css·css3