由【上下文】引出this、作用域链、闭包
JS单线程
- JavaScript是一个单线程语言,意味着同一时间只能执行一个任务。
(函数执行)上下文
概念统一
- 函数执行上下文、执行上下文、上下文、函数执行环境、执行环境、环境、execution context说的是同一种事物
- 本文章会用统一用上下文代称
定义
JS里一切皆对象, 逻辑结构是上下文, 物理结构是对象
js代码在执行阶段前,会有个预加载过程,目的是建立当前js代码的执行环境,而这个执行环境就是上下文(context)
JavaScript是一个单线程语言,意味着同一时间只能执行一个任务。
当JavaScript解释器初始化执行代码时, 它首先默认进入全局执行环境/执行上下文(execution context),从此刻开始,函数的每次调用都会创建一个新的执行环境。
通俗理解: 一段代码和执行所需的所有信息定义为上下文
上下文分类
- 全局环境上下文
- JavaScript代码运行时首次进入的环境, 在web浏览器中全局执行环境是window对象。
- 所有全局变量(除let、const定义的变量)和函数都是作为window对象的属性创建的
- 函数环境上下文——当函数被调用时,会进入当前函数中执行代码。
- Eval上下文——eval内部的文本被执行时(因为eval不被鼓励使用,此处不做详细介绍)。
上下文包含
VO(Variable Object变量声明对象)、AO(Active Object动态变量对象)
- 创建执行上下文阶段: VO, 初始化, 值都为确定, 默认赋为undefined
- 执行函数阶段: AO, 初始化完成, 值都被确定, 依次赋值、关联引用
scopeChain(作用域链) - 可暂时跳过
此处为展开讲解, 和初步理解上下文关系不大, 可跳过
- 定义: 上下文对象的内部属性,包含了函数被创建的作用域中对象的集合
- 作用域链的前端(头部)始终都是当前执行代码所在的上下文(也就是当前活动对象)
- 创建执行上下文阶段: 保证对执行环境有权访问的变量和函数的有序访问
- 执行函数阶段: 每遇到一个变量,就会从作用域链头部开始搜索,查找同名的标识符,如果找到了就使用对应的变量,如果没找到继续搜索作用域链中的上一层对象,一直延续到全局上下文, 如果搜索完都没找到,就认为该标识符未定义。
- 扩展:
- 延长作用域链:执行流进入try-catch语句中的catch块或者with语句都会延长作用域链。这两个语句都会在作用域链的前端添加一个变量对象,with语句会将指定的对象添加到作用域链的顶部,catch语句会创建一个新的变量对象(包含被抛出的错误对象的声明)。
- 没有块级作用域:使用var声明的变量会自动被添加到最接近的环境中(如if、for语句中的var声明的变量,不像其他类C语言由花括号封闭的代码块都有自己的作用域)。如果不使用var声明而是直接初始化就会自动被添加到全局环境中。
this(指针) - 本文后边会展开讲
生命周期
操作 操作 创建阶段 操作 执行阶段 操作 生成变量对象VO 变量赋值AO 调用/触发函数→ 创建执行上下文→ 建立作用域链 →执行函数→ 函数引用AO →函数执行完毕出栈 + 等待被垃圾回收 确定this指向 执行其他代码 上下文是在函数调用/触发的时候被创建的
创建执行上下文阶段
根据函数的参数,创建并初始化arguments object。
扫描上下文的函数声明:
对于找到的函数声明,将函数名和函数引用存入VO(Variable Object)中,如果VO中已经有同名函数(多个函数名相同),那么就进行覆盖。
对于找到的每个变量声明,将变量名存入VO中,并且将变量的值初始化为undefined。如果变量的名字已经在变量对象里存在,不会进行任何操作(忽略)并继续扫描。
function person(age) { var name = 'abby'; var getName = function getName() { }; function getAge() { return age } // 同名函数 function getAge() { return age } } person(20); // 调用/触发person(20)的时候,创建的上下文状态是这样: // 首先是指出函数的引用,然后按顺序对变量进行定义,初始化为undefined personExecutionContext = { scopeChain: { ... }, // 建立作用域链 variableObject: { // 生成变量对象 arguments: { 0: 20, length: 1 }, age: 20, getAge: pointer to function getAge(), name: undefined, getName: undefined, }, this: { ... } // 确定this指向 } // 当上下文创建完成之后,执行流进入函数并且在上下文中运行/解释代码,指定函数的引用和变量的值,如下: personExecutionContext = { scopeChain: { ... }, variableObject: { arguments: { 0: 20, length: 1 }, age: 20, getAge: pointer to function getAge(), name: 'Abby', // 变量赋值 getName: pointer to function getName(), // 变量赋值 }, this: { ... } } ....... // 函数执行完毕出栈 + 等待被垃圾回收
函数声明提升、变量提升
// 上下文创建阶段的过程是:
1.函数name和其引用被存入到VO之中。
2.变量name发现在VO之中存在同名的属性,因此忽略。
3.变量another存入到VO之中,并赋值为undefined。(这也是函数表达式不会提升的原因)
(function() {
console.log(typeof name); // function 声明提升
console.log(typeof another); // undefined 变量提升
// 函数执行阶段, 运行到这里会依次为变量赋值
var name = 'Abby',
another = function() {
return 'Lucky';
};
// 函数执行阶段, 运行到这里会略过函数声明
function name() {
return 'Abby';
}
// 函数执行阶段, name变量的赋值会覆盖函数声明
console.log(typeof name); // string
console.log(name); // 'Abby'
console.log(typeof another); // function
}());
this指针
定义
- this对象是在运行时基于函数的上下文(执行环境)绑定
重点!!!
- 记住一点就行!!! this当函数被调用时才会确定
函数不同调用方式的this指向
作为普通函数在全局环境中被调用
全局环境里面,this 永远指向 window,fn 其实是作为 window 的一个方法被调用的,因此 fn() 实际上就是 window.fn()
作为对象的属性被调用
作为 当前对象 的方法被调用的,因此 this 实际上指向当前对象
但是当在对象obj1的方法(函数)foo1中再定义函数bar时,这时候 this 又是 window, 因为bar并不是obj1的属性(方法)
作为构造函数被调用
作为构造函数被调用的时候,this 代表它即将 new 出来的实例对象。
如果不加 new,表示即作为普通函数调用,指向 window
作为 call/apply/bind 方法的调用
调用的时候指向传入的值
已经使用 bind 绑定过的函数,在调用时使用 call 或 apply 指定一个对象作为 this 会有什么结果?答案是 call 或 apply 指定的对象会被无视。原因并不是 call 或 apply 失效了,而是 bind 过的函数使用闭包保存了绑定时的对象
箭头函数
箭头函数里面 this 始终指向外部对象(包裹函数),因为箭头函数没有 this,因此它自身不能进行new实例化,同时也不能使用 call, apply, bind 等方法来改变 this 的指向。
setTimeout、setInterval中的this
超时调用的代码都是在全局执行域中执行的
this 默认指向 window 对象,除非手动改变 this 的指向
setTimeout 中的回调函数在严格模式下也指向 window 而不是 undefined (是个坑), 可以理解为window.setTimeout
严格模式下
在全局环境中执行函数调用的时候 this 并不会指向 window 而是会指向 undefined
因此调用函数时必须指定所在对象,比如window.fn()
setTimeout 中的回调函数在严格模式下也指向 window 而不是 undefined (是个坑), 可以理解为window.setTimeout
结合5、6、7演示一下
function foo(){ 'use strict' console.log(this) // undefined setTimeout(()=>console.log(this),1000) // undefined setTimeout(function(){console.log(this)},3000) // Window } foo() function bar(){ console.log(this) // Window setTimeout(()=>console.log(this),1000) // Window setTimeout(function(){console.log(this)},3000) // Window } bar()
构造函数 prototype 原型对象属性
在 原型对象中,this 指向的实例对象。即便是在整个原型链中,this 也代表当前实例对象的值。
Eval函数
this 指向当前作用域的对象 和不使用 Eval ,作为对象的方法调用的时候得出的结果是一样的。
闭包
定义
- js中经过特殊封装的函数
- 官方定义: 有权访问另一个函数作用域的变量的函数
- 在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
特点
- 常见形式: 方式便是在一个函数里面嵌套另一个函数,另一个函数持有父作用域里面定义的变量。
- 闭包并不是一定需要 return 某个函数
- 当一个函数里面有闭包的时候,那么这个闭包里面的变量会保存,即使外部已经销毁,但是他依然存在,所以闭包可以保存变量。
- 闭包内引用的变量会存放在堆中,垃圾回收时,不会回收,而已再次引用。
演示
在chrome的本地变量表中清楚的记录着当前执行函数中的本地变量列表,并且还进行了分类,比如上面的”局部函数变量(Local)“,”包含函数变量(Closure)”,“全局变量(Global)”,
那下面有个有趣的问题就来了,chrome怎么知道我代码执行到20行的时候,当前的local variables有哪些呢?而且还能给我分门别类,但是仔细推敲一下就能豁然开朗,肯定有一个变量保存着当前的variables,不然的话,chrome去哪读取呢?
其实在每个function里面都有一个scope属性,当然这个属性被引擎屏蔽了,你是看不见也摸不着的,它里面就保存着当前函数的 local variables,如果应用到上面demo的话,就是全局函数中有一个scope,createComparison有一个scope,匿名的compare有一个scope,而且这三个scope还是通过
链表链接的,简图如下:
上面简图中可以看到,其实整个函数中有三个scope,每个scope都是用next指针链接,这样就形成了一个链表,当我执行下面代码的时候
var result = compare({ name: "d", age: 20 }, { name: "c", age: 27 });
js引擎会拿到当前compare的scope,通过scope属性的next指针,就可以区分哪些变量属于哪个函数,这样你就看到了chrome对variables的分门别类了。
不同形式的闭包实例(5种)
// 1. 最常见的基础闭包
function foo(name) {
let str = `Hello,${name}`;
return function() {
console.log(str);
}
}
let myFoo = sayHello('abby');
myFoo(); // Hello,abby
// 2. 闭包内不return
// 虽然常见的闭包都是 return 出来一个函数,但是闭包并不一定非要 return,return 出一个函数只是为了能在作用域范围之外访问一个变量,我们用另一种方式也能做到,比如:
let say;
function sayHello(name) {
let str = `Hello,${name}`;
say = function() {
console.log(str);
}
}
let myHello = sayHello('abby');
say(); // Hello,abby
// 3. 共享闭包
// 同一个调用函数生成同一个闭包环境,在里面声明的所有函数同时具有这个环境里面自由变量的引用。
// 我们用setUp这个函数生成了一个闭包环境,在这个环境里面的三个函数共享了这个环境里面的 number 变量的引用,因此都可以对 number 进行操作。
let get, up, down
function setUp() {
let number = 20
get = function() {
console.log(number);
}
up = function() {
number += 3
}
down = function() {
number -=2;
}
}
setUp();
get(); // 20
up();
down();
get(); // 21
// 4. 每一个调用函数都会创建不同的闭包环境
function newClosure() {
let array = [1, 2];
return function(num) {
array.push(num);
console.log(`array:${array}`);
}
}
let myClosure = newClosure();
let yourClosure = newClosure();
myClosure(3); // array:1,2,3
yourClosure(4); // array:1,2,4
myClosure(5); // array:1,2,3,5
// 5. 在循环里面创建闭包
function newClosure() {
// 相当于var i = 0;
for(var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
})
}
}
newClosure(); // 5个5
闭包实战
多数手写题会涉及到闭包的使用
flatten打平多维数组(同时使用了函数柯里化)
// 闭包+柯里化形式 function flattenMd1() { let result = []; return function flatten(arr) {// 闭包 arr.forEach(item => { if (Array.isArray(item)) { //如果是数组的话,递归调用 flatten(item); } else { result.push(item); //如果不是数组,直接放入结果中 } }) return result; } } var ary = [1, [2, [3, [4, 5]]], 6]; console.log(flattenMd()(ary)); // 函数柯里化 部分求值 打印结果:[ 1, 2, 3, 4, 5, 6 ] // 当然也有非闭包形式 const arr = [1,2,3,[[[333]]],4,[5,6]] function flattenArr(originArr) { let curArr = [] orifinArr.forEach(item=>{ if(Array.isArray(item)){ curArr = curArr.concat(flattenArr(item)) }else{ curArr.push(item) } }) return curArr } console.log(flattenArr(arr));
防抖、节流
// 防抖函数 const debounce = (fn, delay) => { let timer = null; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }; // 节流函数 const throttle = (fn, delay = 500) => { let flag = true; return (...args) => { if (!flag) return; flag = false; setTimeout(() => { fn.apply(this, args); flag = true; }, delay); }; };
还有很多闭包的实际应用就不一一列举了