函数✅
什么是执行上下文 ?
答案
执行上下文(Execution Contexts) 是规范定义的内部对象, 用来记录当前代码执行时的所有状态信息,包括 this 的绑定、变量的作用域等。是规范定义的内部状态。
执行上下文包含如下核心组件:
组件 | 目的 |
---|---|
通用执行环境状态 | |
code evaluation state | 追踪代码执行状态,包括暂停和恢复执行。例如 Promsie 的状态,迭代器状态等 |
Function | 关联的函数对象(如果是函数执行环境),否则为 null 。 |
Realm | 代码访问 ECMAScript 资源的 Realm 记录。用来做沙箱隔离,比如 Iframe 中访问挂载的 ifram window.Array 和 Iframe 宿主对应的 window.Array 不同 |
ScriptOrModule | 代码来源的 Script 记录或 Module 记录,初始化时可能为 null 。 |
ECMAScript 代码执行环境额外状态 | |
LexicalEnvironment | 词法环境,存储作用域链,用于解析标识符。 |
VariableEnvironment | 变量环境,存储 var 变量的绑定。 |
PrivateEnvironment | 私有环境,存储类的私有字段,若无则为 null 。 |
生成器执行环境额外状态 | |
Generator | 关联的生成器对象,表示正在执行的 Generator。 |
典型的执行上下文包含如下类型
类型 | 触发条件 | 特点 |
---|---|---|
全局执行上下文 | 脚本首次运行时创建 | 包含规范定义的内置对象,和宿主环境的全局对象,以及自定义的全局信息 |
函数执行上下文 | 每次函数调用时创建 | 包含函数作用域、this 绑定、参数等信息 |
Eval 执行上下文 | eval() 代码执行时 | 作用域取决于调用位置,严格模式下有独立作用域 |
执行上下文是 ECMAScript Executable Code and Execution Contexts 章节定义的抽象概念。实际开发中无法直接访问或观察执行上下文,引擎的实现也不一定会完全遵从该定义。但是通过深入理解了执行上下文的机制,可以辅助理解作用域链、闭包、this 等 JavaScript 核心语言特性, 对于 V8 可以参考 context 了解上下文包含的核心信息
延伸阅读
- JavaScript Execution Context 详细讲解了执行上下文和环境记录的关系
- JavaScript Visualized - Execution Contexts 国外博主讲解执行上下文的可视化视频,基于该视频可以更直观地理解执行上下文的概念
说一下 this?
答案
this 是一个关键字,this 的值由当前执行上下文动态绑定,这也是导致 this 的值复杂的原因!this 值指向的核心规则如下
调用方式 | 值的规则 | 注意事项 |
---|---|---|
函数调用 | this 指向全局对象(非严格模式下)或 undefined(严格模式下) | 如果函数是作为对象的方法调用,则 this 指向该对象 |
new 调用 | this 指向新创建的实例对象 | new 调用中 this 无法被修改 |
call/apply 调用 | this 指向第一个参数指定的对象 | 可以显式改变 this 的指向 |
bind 调用 | this 指向第一个参数指定的对象,并返回一个新函数 | 可以显式改变 this 的指向,并预设参数 |
箭头函数 | this 指向定义时所在的上下文(词法作用域) | 箭头函数没有自己的 this,继承外层作用域的 this(这也是为什么无法修改箭头函数 this 的原因),而外层作用域 this 取决于运行时的执行上下文 |
使用注意事项:
- 将对象方法作为回调函数或者引用传递时,会导致 this 丢失,因为此时调用方式变成了普通函数调用而不是成员方法调用,此时 this 会指向全局对象或 undefined。
- 箭头函数的 this 绑定是固定的取决于外层作用域的 this, 但是值任然由执行上下文动态绑定,不要错误的认为箭头函数的 this 是不可变的
- 严格模式下,未绑定的 this 为 undefined
示例说明
什么是作用域 ?
答案
作用域是指 js 访问变量,函数等标识符的规则,这些规则决定了标识符的可见性和生命周期。 常见的 JS 作用域包括。
- 全局作用域:在全局作用域中定义的变量和函数可以在任何地方访问。浏览器中是
window
对象,Node.js 中是global
对象。 - 函数作用域:在函数内部定义的变量和函数只能在该函数内部访问。每次调用函数时,都会创建一个新的函数作用域。
- 块级作用域:在 ES6 中引入的块级作用域,使用
let
和const
声明的变量只能在其所在的代码块内访问。 - 模块作用域:在 ES6 中引入的模块作用域,每个模块都有自己的作用域,模块内的变量和函数默认是私有的,除非显式导出。
with,eval
作用域:with
语句和eval
会动态在当前作用域中注入新的绑定, 注意在严格模式下不支持with
, 同时eval
也会创建新的作用域,不会对外部作用域造成影响。
作用域会形成嵌套结构,在访问对应变量或函数等标识符时,会基于作用域链逐级向上找到第一个匹配的标识符名称,所以当存在同名的标识符的时候,内层作用域会覆盖外层作用域的同名标识符。
V8 等引擎支持的作用域类型,详见 ScopeType。在规范中作用域的信息实际上叫做 环境记录(Environment Record)
, 而作用域链逐级查找的规则,可以查看 GetIdentifierReference 的定义。
参考示例
什么是闭包?
答案
闭包是指内部函数持有对外部函数作用域变量的引用,即使外部函数已经执行完毕,这些变量也不会被垃圾回收,从而实现数据的持久化和封装。
闭包的典型使用场景包括
- 实现私有变量和方法,保护数据不被外部直接访问
- 回调和异步操作中保存状态
- 函数柯里化和高阶函数
- 实现缓存功能
使用必包时需要注意以下几点:
- 闭包会导致其引用的外部变量常驻内存,可能造成内存泄漏,尤其是在循环或大量创建闭包时需注意释放引用。
- 闭包访问的变量是引用而非副本,若变量被修改,所有闭包实例都会感知变化。
规范中并未详细提及必包的概念,但是在 ECMAScript Function Objects 函数定义中包含了 [[Environment]]
的内置属性,这使得嵌套函数有能力通过,Environment Records
的 [[OuterEnv]]
属性访问到外层的执行上下文的变量。此外在引擎具体的实现时,只会对消费到的变量进行闭包化处理,以此减少内存开销。
示例说明
构造函数和普通调用有什么区别?
答案
普通函数是指直接调用的函数,而构造函数是通过 new
关键字调用的函数。从规范的角度普通函数调用内部的 [[Call]]
方法,构造函数调用内部的 [[Construct]]
方法。
核心区别实际上就是执行上下文的状态差异,包含
- this 的指向
- 普通函数调用时,this 指向全局对象(非严格模式下)或 undefined(严格模式下)。
- 构造函数调用时,this 指向新创建的实例对象。
- 返回值
- 普通函数调用时,返回值是函数执行的结果。
- 构造函数调用时,返回值是新创建的实例对象,除非显式返回一个对象。
- 额外属性注入
- 构造函数会注入
super, new.target
等额外属性到执行上下文中。
- 构造函数会注入
示例说明
JS 有几种创建函数的方式,有哪些区别 ?
答案
创建方式 | 语法 | 特点 |
---|---|---|
函数声明 | function functionName() {} | 具名函数,提升到作用域顶部,可以在声明之前调用 |
函数表达式 | const functionName = function() {} | 匿名函数,不能提升,必须在定义后调用 |
箭头函数 | const functionName = () => {} | 匿名函数,简洁语法,没有自己的 this,继承外层作用域的 this |
函数构造器 | new Function('arg1', 'arg2', 'return ...') | 动态创建函数,参数为字符串形式,不推荐使用,性能较差 |
eval | eval('functionName = function() {}') | 动态执行字符串代码,存在安全隐患和性能问题,不推荐使用 |
类方法 | class ClassName { methodName() {} } | 类的实例方法,具名函数,提升到类定义之前,可以在类实例上调用 |
示例说明
用过哪些函数对象的方法?
答案
分类 | 名称 | 说明 | 常用 |
---|---|---|---|
静态方法 | Function.prototype | 所有函数的原型对象 | ✔️ |
Function.length | 形参个数 | ✔️ | |
Function.name | 函数名 | ✔️ | |
Function.apply() | 指定 this 调用函数,参数为数组 | ✔️ | |
Function.call() | 指定 this 调用函数,参数逐个传递 | ✔️ | |
Function.bind() | 返回绑定 this 的新函数 | ✔️ | |
Function.toString() | 返回函数源码字符串 | ✔️ | |
Function.isGeneratorFunction() | 判断是否生成器函数(部分环境支持) | ❌ | |
原型方法 | Function.prototype.constructor | 构造函数引用自身 | ✔️ |
Function.prototype.apply() | 同上,所有函数实例可用 | ✔️ | |
Function.prototype.call() | 同上,所有函数实例可用 | ✔️ | |
Function.prototype.bind() | 同上,所有函数实例可用 | ✔️ | |
Function.prototype.toString() | 同上,所有函数实例可用 | ✔️ | |
属性 | arguments | 函数内部类数组对象,包含所有实参(非箭头函数) | ✔️ |
caller | 调用当前函数的函数(非严格模式下) | ❌ | |
prototype | 构造函数的原型对象,实例共享 | ✔️ |
函数表达式和函数声明的区别?
答案
区别 | 函数声明 (Function Declaration) | 函数表达式 (Function Expression) |
---|---|---|
语法 | function foo() {} | const foo = function() {} 或 const foo = () => {} |
是否提升 | 是,声明提升到作用域顶部 | 否,只有赋值语句被提升,函数体不会提升 |
调用时机 | 可以在声明前调用 | 只能在定义后调用 |
是否必须命名 | 必须有函数名 | 可匿名或具名 |
作用域 | 当前作用域 | 赋值变量的作用域 |
this 绑定 | 动态绑定 | 动态绑定(普通表达式)/ 词法绑定(箭头函数) |
调试栈显示 | 显示函数名 | 匿名函数无名,具名表达式有名 |
用途 | 通常用于全局/局部函数定义 | 常用于回调、IIFE、赋值等 |
说一下箭头函数?
答案
箭头函数(Arrow Function)是 ES6 引入的一种简洁函数写法,主要为了解决传统函数中 this 指向混乱、代码冗长等问题。
箭头函数的语法如下:
// 采用 => 定义箭头函数
const add = (a, b) => a + b
箭头函数的特点:
特性 | 普通函数 | 箭头函数 |
---|---|---|
this | 调用时动态绑定 | 定义时静态绑定 |
arguments | 有自己的 | 没有自己的 |
作为构造函数 | 可以(可 new) | 不可以 |
prototype 属性 | 有 | 没有 |
yield | 可用(可生成器) | 不可用 |
super/new.target | 支持 | 不支持 |
示例说明
call,apply,bind 区别?
答案
call、apply 和 bind 都是 JavaScript 中用于改变函数执行上下文(即 this 指向)的方法。
方法 | 语法格式 | 作用 | 返回值 | 传参方式 | 是否立即执行 | 支持版本 |
---|---|---|---|---|---|---|
call | func.call(thisArg, arg1, arg2, ...) | 改变 this 指向并调用函数 | 函数执行结果 | 依次传递参数 | 是 | ES3+ |
apply | func.apply(thisArg, [argsArray]) | 改变 this 指向并调用函数 | 函数执行结果 | 参数以数组形式传递 | 是 | ES3+ |
bind | func.bind(thisArg[, arg1[, arg2[, ...]]]) | 创建绑定 this 的新函数 | 新函数 | 依次传递参数(可预设部分参数) | 否 | ES5+ |
IIFE 是什么?
答案
IIFE 是立即执行函数表达式。
要理解 IIFE 必须知晓函数申明和函数表达式的区别。一个最简单的判别方法是若 function 关键字未出现一行的开头则且不属于上一行的组成部分则为函数申明否则为函数表达式。
所以只要函数表达式被立即调用则称之为 IIFE.
此外函数表达式和函数申明具有如下区别
- 函数表达式不会被提升
- 函数表达式的标识符是可省略的
- 函数表达式的标识符作用域为该函数体,外部执行环境无法使用
- 具名函数表达式的名称作为函数名,匿名函数名称取决于申明方式
推荐阅读:
eval 了解多少?
答案
eval()
是 JavaScript 的全局函数,用于将字符串当作代码在当前作用域内解析并执行。它的基本用法如下:
const x = 1
const y = 2
const result = eval('x + y') // 3
- 动态执行代码:可以在运行时动态生成并执行 JavaScript 代码。
- 访问当前作用域:
eval
执行的代码可以访问当前作用域的变量和函数。 - 动态表达式计算:常用于需要根据字符串动态计算表达式的场景。
注意事项
- 安全风险极高:
eval
能执行任意字符串代码,若字符串内容可控,极易被注入恶意代码,造成 XSS 等安全漏洞。 - 性能较差:每次调用
eval
都需要解析和编译字符串,无法享受编译优化,执行效率远低于静态代码。 - 调试困难:
eval
生成的代码在调试和错误定位时不直观,增加维护难度。 - 作用域混淆:
eval
代码可访问和修改当前作用域变量,易引发作用域混乱和难以追踪的 bug。 - 严格模式限制:在严格模式下,
eval
的作用域更加受限,不能影响外部作用域。
eval
代码在运行时动态解析和编译,无法被引擎提前优化,频繁使用会显著拖慢性能。- 简单、偶尔使用时影响不大,但复杂或高频场景下应避免。
- 静态代码可被编译器优化,执行更快且资源消耗更低。
优势
- 极高灵活性:可动态生成和执行代码,适合表达式求值、元编程等特殊场景。
- 可访问当前作用域:与
Function
构造器不同,eval
可直接操作当前作用域变量。 - 动态命名空间操作:可用于动态定义变量、函数等。
eval
功能强大但风险极高,实际开发中应极力避免使用。绝大多数需求可用更安全的替代方案(如 JSON.parse
、Function
构造器、模板引擎等)实现。仅在完全可控、无安全隐患的场景下谨慎使用。
参考资料: