JS引擎在解析JS代码的时候并非是逐行解析,而是自上而下一块一块地去解析的,在解析过程中就会去创建这一块代码的执行上下文。
执行上下文分为:全局执行上下文、函数执行上下文、eval执行上下文
- 全局执行上下文:JS引擎在编译全局代码时,创建全局执行上下文;
- 函数执行上下文:在函数执行的前一刻,JS引擎会创建一个函数执行上下文;
- eval执行上下文:在执行eval代码块前,会创建一个独立的eval执行上下文。
执行上下文的创建分为两个阶段:预编译阶段、执行阶段
执行上下文创建会做两件事情:
- 创建词法环境 LexicalEnvironment
- 创建变量环境 VariableEnvironment
1、在预编译阶段JavaScript引擎会初始化词法环境和变量环境
- let/const变量,在初始化时会被置为标志位,在没有执行到
let xxx 或 let xxx = ???
时,提前读取变量会报ReferenceError的错误,这个特性又叫暂时性死区
- var变量,在初始化时会先被赋值为undefined,这就是为什么我们在还没有执行到
var xxx 或 var xxx = ???
时,提前读取变量时不会报错,尽管它的值为undefined,这个特性叫做变量提升
- 函数变量,在初始化的时候就会被直接赋值为这个函数,这也是为什么我们能够在还没有执行到
function func(){}
时,依然能够执行func()
函数的原因,这个特性叫做函数提升
2、在执行阶段对初始化的变量进行赋值并按序执行
词法环境和变量环境
官方定义:词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符与特定变量和函数的关联关系.
变量环境也是一种词法环境,词法环境组件和变量环境组件的区别在于:
- 词法环境存储
let
const
class
等除了var
函数
声明之外的所有变量, 而变量环境存储var
函数
声明的变量- 执行代码块内的 let、const、class 声明的标识符合集记录为 lexNames
- 执行代码块内的 var 和 function 声明的标识符合集记录为 varNames
- 如果 lexNames 内的任何变量在 varNames 或 lexNames 内出现过,则报错 SyntaxError
这就是为什么可以用 var、function 声明多个同名变量,但是不能用 let、const、class 声明多个同名变量
let、const、class
等声明的变量被置为 标志位,并没有被初始化赋值,在运行至其声明处代码时才会进行初始化这就是我们所说的暂时性死区,
let、const、class
声明的变量并没有被初始化赋值,而是被置为 标志位, 所以在声明前访问会报错var
声明的变量初始化赋值为undefined,如果有同名变量则跳过这就是所谓的变量提升,我们用
var
声明的变量,在声明位置之前访问并不会报错,而是返回undefinedfunction
声明的变量变量初始化赋值为对应的函数体,如果有同名function
声明的变量,前面的都会忽略,取最后一个声明的函数声明会被直接赋值,所有我们在函数声明位置之前也可以调用函数
- varNames 内的 function 声明的变量和 var 声明的变量,如果同名,则 var 声明的变量会被忽略
即会被赋值为一个函数体而不是undefined,即使 var 声明的位置在 function 声明位置的后面
- 如果 lexNames 内的任何变量在 varNames 或 lexNames 内出现过,则报错 SyntaxError
词法环境由三个个部分组成:
- 环境记录(EnvironmentRecord):存储变量和函数声明的实际位置
- 对外部词法环境的引用(Outer Reference):提供了访问父词法环境的引用,可能为null
- this绑定(ThisBinding):确定当前环境中this的指向
词法环境有三种类型
- 全局词法环境,是整个词法环境链的顶端,它的外部词法环境值为null
- 全局词法环境的环境记录中会绑定一个全局对象,用于存储
- 函数词法环境,在函数执行的前一刻会创建函数词法环境
- 函数词法环境的外部词法环境指向调用该函数的父环境
- 模块词法环境,在模块词法环境中你可以读取到export、module等变量,这些变量都是记录在模块环境的环境记录中
- 模块词法环境的外部词法环境指向全局环境
环境记录(EnvironmentRecord)
代码中声明的变量和函数都会存放在环境记录(EnvironmentRecord)中等待执行时访问
环境记录有两个不同类型,分别为object和declarative
- 对象环境记录(object),用来定义那些将标识符与某些对象相绑定的JS语法元素
- 比如全局对象就定义在对象环境记录中,一些内置对象如Array、Object等和内置方法如eval、parseInt等和全局变量都定义在全局对象中
- 声明式环境记录(declarative),用来定义那些直接将标识符与语言值绑定的JS语法元素
我们常说的作用域其实说的就是词法环境,全局作用域就是全局词法环境,函数作用域就是函数词法环境,块级作用域就是模块词法环境
对外部词法环境的引用(Outer Reference)
- 全局词法环境,它的外部词法环境值为null
- 函数词法环境,它的外部词法环境指向调用该函数的父环境
- 模块词法环境,它的外部词法环境指向全局环境
- 在查找变量的时候,会先在自己的词法环境中查找,如果找不到就到引用的外部词法环境中查找,沿着这条对外部词法环境的引用路线一直向上,直到找到或者到达全局词法环境为止,这条对外部词法环境的引用的链路就叫做词法环境链,也叫做作用域链
this绑定(ThisBinding)
默认绑定
在全局执行上下文中,this 的值绑定为全局对象
// 获取全局对象
function getGlobalObject() {
if (typeof globalThis !== 'undefined') { return globalThis; }
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('cannot find the global object');
}
// 更可靠的获取全局对象
(function() {
if (typeof globalThis === 'object') return;
Object.defineProperty(Object.prototype, '__magic__', {
get: function() {
return this;
},
configurable: true // This makes it possible to `delete` the getter later.
});
__magic__.globalThis = __magic__; // lolwat
delete Object.prototype.__magic__;
}())
在函数执行上下文中,this 的值取决于函数的调用方式。如果它被一个对象引用调用,那么 this 的值被设置为该对象,否则 this 的值在非严格模式下被绑定为为全局对象,在严格模式下this 的值为undefined
箭头函数
- 1、箭头函数内部没有绑定this,所以无法手动指定this
- 2、箭头函数没有原型,因而不能作为构造函数,不能使用new来创建实例对象
- 3、箭头函数不绑定Arguments 对象,但是可以使用剩余参数(...rest)
- 4、在箭头函数执行前,会通过它的词法环境链来确定它的this值,因而箭头函数内部的this是其定义位置的this,而不是调用位置的this
手动绑定
通过call/apply/bind能够手动绑定函数内部的this
- Function.prototype.call
- 调用方式:function.call(thisArg, [arg1, arg2, ...])
- call方法能够为函数手动绑定this和传入参数列表,并且在绑定之后立即执行该函数
- Function.prototype.apply
- 调用方式:func.apply(thisArg, [argsArray])
- apply方法能够为函数手动绑定this和传入参数数组,并且在绑定之后立即执行该函数
- Function.prototype.bind
- 调用方式:function.bind(thisArg, [arg1, arg2, ...])
- bind方法能够为函数手动绑定this和传入参数列表,但是在在绑定之后不会立即执行该函数
原生实现 call apply bind
// call
// thisBinding 绑定this
// call方法传递的是参数列表
Function.prototype.myCall = function(thisBinding){
// 只允许函数调用call方法
if (typeof this !== 'function') throw new TypeError(`${this} is not a function`)
// 确定绑定this是一个对象
// 绑定this为null时,将其指定为全局对象
// 绑定this为原始类型,将其处理为引用类型
let context = thisBinding == null ? globalThis : typeof thisBinding !== 'object' ? Object(thisBinding) : thisBinding
// 把函数赋值到绑定this的属性
// 使用Symbol防止使这个属性具有唯一性,这样
// 既不会影响绑定this原本的属性值,比如原本绑定this有一个属性a,在这里使用a就会将其覆盖掉,改变了绑定this的原本属性结构
// 也不会被绑定this所影响,比如原本绑定this有一个属性a在某种情况下被修改了,有可能会影响到这里的操作
let key = Symbol('KEY')
context[key] = this
// 基于“对象[成员]()”方法把函数执行,此时函数中的this就是绑定this了
// 获取参数列表
const paramsList = [...arguments].slice(1)
// 把参数传递给函数,并且接收返回值
const result = context[key](...paramsList)
/* 这里也可以直接使用剩余参数的形式而不用使用arguments,然后需要在参数出多传一个剩余参数function(thisBinding, ...rest){...}
const result = context[key](...rest)
*/
// 函数执行完毕后将这个属性删除,恢复绑定this的原本结构
delete context[key]
//把函数的返回值作为call方法执行的结果返回
return result
}
// apply
// thisBinding 绑定this
// apply传递的是参数数组
Function.prototype.myApply = function(thisBinding){
// 只允许函数调用call方法
if (typeof this !== 'function') throw new TypeError(`${this} is not a function`)
// 确定绑定this是一个对象
// 绑定this为null时,将其指定为全局对象
// 绑定this为原始类型,将其处理为引用类型
let context = thisBinding == null ? globalThis : typeof thisBinding !== 'object' ? Object(thisBinding) : thisBinding
// 把函数赋值到绑定this的属性
// 使用Symbol防止使这个属性具有唯一性,这样
// 既不会影响绑定this原本的属性值,比如原本绑定this有一个属性a,在这里使用a就会将其覆盖掉,改变了绑定this的原本属性结构
// 也不会被绑定this所影响,比如原本绑定this有一个属性a在某种情况下被修改了,有可能会影响到这里的操作
let key = Symbol('KEY')
context[key] = this
// 基于“对象[成员]()”方法把函数执行,此时函数中的this就是绑定this了(把参数传递给函数,并且接收返回值)
const paramsList = [...arguments].slice(1)
const result = context[key](paramsList)
// 函数执行完毕后将这个属性删除,恢复绑定this的原本结构
delete context[key]
//把函数的返回值作为call方法执行的结果返回
return result
}
// bind
// thisBinding 绑定this
// bind方法传递的是参数列表
// bind方法不会立即执行函数
Function.prototype.myBind = function(thisBinding){
// 只允许函数调用call方法
if (typeof this !== 'function') throw new TypeError(`${this} is not a function`)
// 确定绑定this是一个对象
// 绑定this为null时,将其指定为全局对象
// 绑定this为原始类型,将其处理为引用类型
let context = thisBinding == null ? globalThis : typeof thisBinding !== 'object' ? Object(thisBinding) : thisBinding
// 把函数赋值到绑定this的属性
// 使用Symbol防止使这个属性具有唯一性,这样
// 既不会影响绑定this原本的属性值,比如原本绑定this有一个属性a,在这里使用a就会将其覆盖掉,改变了绑定this的原本属性结构
// 也不会被绑定this所影响,比如原本绑定this有一个属性a在某种情况下被修改了,有可能会影响到这里的操作
let key = Symbol('KEY')
context[key] = this
// 基于“对象[成员]()”方法把函数执行,此时函数中的this就是绑定this了
// 获取参数列表
const paramsList = [...arguments].slice(1)
// 只绑定this,不立即执行函数
// ...args 函数执行时传递的参数
return function(...args){
// 把参数传递给函数,并且接收返回值
const result = context[key](...paramsList.concat(args))
// 函数执行完毕后将这个属性删除,恢复绑定this的原本结构
delete context[key]
//把函数的返回值作为call方法执行的结果返回
return result
}
}
// 利用call或者apply简写
Function.prototype.myBind = function myBind(thisBinding){
const _this = this
const paramsList = [...arguments].slice(1)
return function(...agrs){
return _this.call(thisBinding, ...paramsList.concat(agrs))
}
}
执行上下文创建之后就会被推入执行环境栈中存储起来,每创建一个新的执行上下文时都会被推入当前执行环境栈的顶端,等到代码执行完毕后,其对应的执行上下文就会从执行环境栈中弹出,上下文控制权将移到当前执行环境栈的下一个执行上下文
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
function third() {
console.log('Inside third function');
}
return third
}
function second() {
console.log('Inside second function');
}
var f = first();
f()
console.log('Inside Global Execution Context');
// 当上述代码在浏览器中加载时
/*
1、创建一个全局执行上下文,并将其推入执行环境栈
2、执行 first 函数时,Javascript 引擎为该函数创建了一个新的执行上下文,并将其推到当前执行环境栈的顶端
3、当在 first 函数中执行 second 函数时Javascript 引擎为该函数创建了一个新的执行上下文,并将其推到当前执行环境栈的顶端
4、second 函数执行完成后,它的执行上下文从当前执行环境栈中弹出,上下文控制权将移到当前执行环境栈的下一个执行上下文,即 first 函数的执行上下文
5、first 函数执行完成后,它的执行上下文从当前执行环境栈中弹出,上下文控制权将移到当前执行环境栈的下一个执行上下文,即全局执行上下文
6、执行 f 函数,即执行 third 函数,Javascript 引擎为该函数创建了一个新的执行上下文,并将其推到当前执行环境栈的顶端
7、third 函数执行完成后,它的执行上下文从当前执行环境栈中弹出,上下文控制权将移到当前执行环境栈的下一个执行上下文,即全局执行上下文
8、一旦所有代码执行完毕,Javascript 引擎把全局执行上下文从执行栈中移除。
*/
闭包
当函数可以记住并访问其声明位置的词法环境时,就产生了闭包,即使该函数是在当前词法环境之外执行
无论通过何种方式将内部函数传递到其声明位置的词法环境之外,它都会持有对其所在位置的词法环境的引用,而且无论在何处执行这个函数,都会使用到闭包
所以说闭包其实是内部函数对外部词法环境的引用,即使外部词法环境所在的执行上下文已经被回收,但是这个内部函数对外部词法环境的引用依然存在于内存中,所以滥用闭包可能会导致内存泄漏
闭包的应用场景
闭包的特点就是能够访问到函数声明位置的词法环境,不论这个函数在哪里调用,利用这个特性我们可以:
- 1、封装私有变量和方法
function module(){
let var1 = 1
function fnc1()
return {
var1,
fnc1
}
}
let myModule = module()
2、实现防抖和节流
function throttle(fn, interval) {
// last为上一次触发回调的时间
let last = 0
// 将throttle处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
// 记录本次触发回调的时间
let now = +new Date()
// 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
if (now - last >= interval) {
// 如果时间间隔大于我们设定的时间间隔阈值,则执行回调
last = now;
fn.apply(context, args);
}
}
}
function throttle(fn, delay) {
// last为上一次触发回调的时间, timer是定时器
let last = 0, timer = null
// 将throttle处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
// 记录本次触发回调的时间
let now = +new Date()
// 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
if (now - last >= delay) {
// 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
last = now
fn.apply(context, args)
} else {
// 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
clearTimeout(timer)
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, delay)
}
}
}
3、实现once函数
function once(fn){
let called = fasle
return function (){
if(!called){
called = true
fn.apply(this, arguments)
}
}
}
let b = once(function(){})
b()
4、实现记忆函数,用来缓存一些大数组,避免多次循环浪费性能
function memoize(list){
let map = Object.create(null)
list.forEach(item => {
map[item] = true
})
return function(arg){
return !!map[arg]
}
}
感谢 @拳打南山敬老院 / @Tusi / @ Logan70 / @zhangwinwin / @无邪 提供的优质文章