我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
本文将结合 ECMAScript 规范,探讨与函数相关的概念,包括作用域、闭包、this绑定、执行上下文等。并且深入解析从函数定义、函数传递、到函数调用过程中发生了什么。
函数定义
函数有如下几种定义方式:
- 函数声明:会提升到作用域顶部。(不建议在块级作用域内定义函数声明,参考)
- 函数表达式:
- 匿名函数表达式。
- 具名函数表达式:一般用作递归函数。
- 箭头函数表达式:没有this、没有super、没有argument、不能使用new调用。
- new Function():
[[Environment]]指向全局环境记录。
function sayHi() {
alert( "Hello" );
}
const sayHi = function() {
alert( "Hello" );
}
const sayHi = function hi(n) {
alert( "Hello" );
}
const sayHi = () => {
alert( "Hello" );
}
const sum = new Function('a', 'b', 'return a + b');
实例化函数对象步骤:
- 创建函数对象:Function的实例。
- 设置函数对象的属性;
- 设置
[[Environment]]属性为当前执行上下文的词法环境。 - 设置其他属性:函数名称、参数列表、函数体、严格模式、this模式等。
- 设置函数
[[Call]]方法。(普通调用) - 设置函数
[[Construct]]方法。(构造函数调用,箭头函数没有此步骤)
- 设置
- 返回函数对象。
函数表达式在表达式求值时实例化,函数声明在脚本、模块、函数执行之初实例化。
具名函数的
[[Environment]]属性比较特殊,是一个[[OuterEnv]]为当前环境记录的新建环境记录,这个环境记录里只有函数名称一个标识符,且是只读的,用来递归调用函数自身。
作用域与闭包
环境记录
下面是截取自 ECMAScript 规范对环境记录的描述:
环境记录(Environment Record)是一种规范类型,用于定义标识符与特定变量和函数的关联,基于 ECMAScript 代码的词法嵌套结构。通常,环境记录与 ECMAScript 代码的某些特定语法结构相关联。
每个环境记录都有一个
[[OuterEnv]]字段,该字段为空或对外部环境记录的引用。这用于对环境记录值的逻辑嵌套进行建模。环境记录是纯粹的规范机制,不需要对应于 ECMAScript 实现的任何特定工件。 ECMAScript 程序不可能直接访问或操作这些值。
每个声明性环境记录都与一个包含变量、常量、let、类、模块、导入和/或函数声明的 ECMAScript 程序作用域相关联。声明性环境记录绑定由包含在其作用域内的声明定义的标识符集。
ECMAScript 程序中的每个作用域(全局、模块、函数、代码块)都有一个环境记录与之对应。
作用域嵌套就是通过环境记录的[[OuterEnv]]字段对外部环境记录的引用来实现的。
代码在作用域内查找标识符,其实就是在环境记录中查找字段,如果环境记录中找不到就会通过[[OuterEnv]]去外部环境记录中递归查找,直到全局环境记录([[OuterEnv]]为null)。都找不到就返回一个引用错误(严格模式下)。
函数作用域
函数对象实例在创建时会在[[Environment]]字段中引用创建它的作用域对应的环境记录。
函数执行时会创建一个自己的环境记录,环境记录的[[OuterEnv]]字段就取自函数的[[Environment]]字段。所以函数作用域的外部作用域是函数定义时的作用域,而不是调用时的作用域。
闭包
下面是维基百科对闭包的定义:
闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用。
结合上文我们可以知道,ECMAScript 语言的函数本身就是按照闭包来设计的,因为函数对象保存了外部环境记录,可以说每个函数都是闭包。
但是我们通常说的闭包也许并不是普遍意义上的闭包,而是对闭包的一种特殊用法。
我把它称之为出走的闭包。函数对象通过各种途径(比如:作为函数返回值、赋值给其他变量、作为回调函数传参)离开了当前作用域(带着对当前环境记录的引用),当前作用域内的代码执行完成,销毁上下文时,环境记录无法销毁,而出走的闭包任然可以访问到这些变量。
执行上下文
执行上下文是一种规范对象,用于跟踪 ECMAScript 实现对代码的运行时执行。在任何时间点,只会有一个执行上下文在执行,称为正在运行的执行上下文。
执行上下文堆栈(调用栈)用于跟踪执行上下文。正在运行的执行上下文始终是此堆栈的顶部元素。每当控制转移时,就会创建一个新的执行上下文。新创建的执行上下文被压入堆栈,成为运行的执行上下文。
函数调用
普通函数调用
(class构造函数不能使用普通调用)
- 创建执行上下文,初始化上下文属性:
[[ScriptOrModule]]:函数体。[[LexicalEnvironment]]: 创建一个[[OuterEnv]]为[[Environment]]的环境记录。
- 压入调用栈。
- 绑定this。(绑定规则见下文)
- 声明实例化。函数的参数、执行过程中声明的标识符都会绑定到上下文的
[[LexicalEnvironment]]字段上。 - 执行函数体。
- 从调用栈弹出上下文。控制权交给之前的上下文。
- 返回result或undefined;
构造函数调用(new 调用)
(箭头函数不能使用new调用)
- 创建执行上下文,初始化上下文属性:
[[ScriptOrModule]]:函数体。[[LexicalEnvironment]]: 创建一个[[OuterEnv]]为[[Environment]]的环境记录。
- 压入调用栈。
- 初始化this:创建一个原型为F.prototype的对象。(派生类由super完成)
- 声明实例化:函数的参数、执行过程中声明的标识符都会绑定到上下文的
[[LexicalEnvironment]]字段上。 - 执行函数体。
- 从调用栈弹出上下文。控制权交给之前的上下文。
- 如果有返回值且为对象直接返回,否则返回this。
this绑定规则
由于构造函数调用会创建this。我们现在只考虑普通函数调用。
规范层面的函数调用方法接受两个个参数 ([[Call]] ( thisArgument, argumentsList )`)。
结合上文提到的this模式,绑定this的步骤如下:
- this模式是lexil(箭头函数),不作this绑定,从外部作用域获取this。
- this模式是strict(严格模式),采用传入的thisArgument。
- this模式是global(非严格模式),如果thisArgument为undefined或null,采用globalThis,否则使用 ToObject(thisArgument)。
传入的thisArgument有几种情况:
- 显示指定:
- 一次性:apply、call。
- 永久:bind。
- 隐式指定:
- 如果函数调用操作符“
()”左侧的表达式是“引用记录”(比如obj.prop(),()左侧的obj.prop是一个引用记录):thisArgument指定为从引用记录获取的对象。 - 否则:thisArgument指定为undefined。
- 如果函数调用操作符“
let foo = function foo() {
return this;
}
let bar = function bar() {
"use strict";
return this;
}
alert(foo()); // window
alert(bar()); // undefined
let obj = {
tag: "jade",
getTag() {return this.tag},
}
let foo = obj.getTag;
alert(obj.getTag()); // "jade"
alert(foo()); // undefined
let obj = {
tag: "jade",
getTag() {return this.tag},
}
let foo = obj.getTag;
alert(foo.call(obj)); // "jade"
alert(foo(obj)); // undefined
foo = foo.bind(obj);
alert(foo(obj)); // "jade"
let obj = {tag: "jade"};
obj.foo = () => this.tag;
obj.bar = function() {return this.tag;}
window.tag = "little"
alert(obj.foo()); // "little"
alert(obj.bar()); // "jade"
以上我们只讨论了函数作用域内的this绑定。既然this对象是绑定在词法环境对象上的,是不是所有作用域内都有this对象呢?其他作用域内的this是什么值呢?
块级作用域内没有this对象,跟箭头函数一样会从外部作用域获取this。 全局作用域this是globalThis对象,模块作用域的this是undefined。
尾部调用优化
当函数嵌套调用时,如果嵌套函数调用是在外部函数的尾部,也就是return语句。
function foo() {
return bar();
}
那么该嵌套函数的返回值可以直接作为外部函数的返回值,且外部函数没有其他逻辑需要执行了,就可以直接弹出外部函数的上下文,再压入嵌套函数上下文,调用栈就不会增加元素。该优化在递归调用的函数中尤其有效。