1.理解词法作用域和动态作用域
作用域:分为词法作用
关于作用域:
- 定义:作用域是指程序源代码中定义变量的区域。
- 作用:作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
- (1)为了避免名称冲突;
- (2)为了限定变量的生命周期
- 在javaScript中的应用 :JavaScript采用词法作用域(lexical scoping),也就是静态作用域。
作用域气泡由其对应的作用域块代码写在哪里决定,它们是逐级包含的
气泡1包含着整个全局作用域,其中只有一个标识符:foo
气泡2包含着foo所创建的作用域,其中有三个标识符:a、bar和b
气泡3包含着bar所创建的作用域,其中只有一个标识符:c
词法作用域:
词法作用域就是定义在词法阶段的作用域,是由写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定
动态作用域:
动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套
下面的例子看区别:
var a = 2;
function foo() {
console.log( a );
}
function bar() {
var a = 3;
foo();
}
bar();
- 如果处于词法作用域,也就是现在的javascript环境。变量a首先在foo()函数中查找,没有找到。于是顺着作用域链到全局作用域中查找,找到并赋值为2。所以控制台输出2
- 如果处于动态作用域,同样地,变量a首先在foo()中查找,没有找到。这里会顺着调用栈在调用foo()函数的地方,也就是bar()函数中查找,找到并赋值为3。所以控制台输出3
区别总结: 词法作用域是在定义时确定的,而动态作用域是在运行时确定的
https://www.cnblogs.com/xiaohuochai/p/5700095.html
2.理解JavaScript的作用域和作用域链
原文:https://www.cnblogs.com/lhb25/archive/2011/09/06/javascript-scope-chain.html
3.理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题
一、执行上下文(Execution Context)
1.什么是执行上下文
简而言之,执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行
2.执行上下文的类型
执行上下文总共有三种类型:
- 全局执行上下文: 这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创建一个全局对象,在浏览器中这个全局对象就是 window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
- 函数执行上下文: 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在任意数量的函数执行上下文。每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤,具体过程将在本文后面讨论。
- Eval 函数执行上下文: 运行在 eval 函数中的代码也获得了自己的执行上下文,但由于 Javascript 开发人员不常用 eval 函数,所以在这里不再讨论。
二、执行上下文的生命周期
执行上下文的生命周期包括三个阶段:创建阶段 → 执行阶段 → 回收阶段,本文重点介绍创建阶段。
- 创建阶段
当函数被调用,但未执行任何其内部代码之前,会做以下三件事:
创建变量对象:首先初始化函数的参数 arguments,提升函数声明和变量声明。下文会详细说明。
创建作用域链(Scope Chain):
在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。
确定 this 指向:包括多种情况,下文会详细说明
在一段 JS 脚本执行之前,要先解析代码(所以说 JS 是解释执行的脚本语言),解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来。变量先暂时赋值为 undefined,函数则先声明好可使用。这一步做完了,然后再开始正式执行程序。
另外,一个函数在执行之前,也会创建一个函数执行上下文环境,跟全局上下文差不多,不过 函数执行上下文中会多出 this arguments 和函数的参数。
- 执行阶段
执行变量赋值、代码执行 - 回收阶段
执行上下文出栈等待虚拟机回收执行上下文
三、变量提升和 this 指向的细节
1. 变量声明提升
console.log(a); // undefined
var a = 10;
上述代码正常输出undefined而不是报错Uncaught ReferenceError: a is not defined,这是因为声明提升(hoisting),相当于如下代码:
var a; //声明 默认值是undefined “准备工作”
console.log(a);
a = 10; //赋值
2. 函数声明提升
我们都知道,创建一个函数的方法有两种,一种是通过函数声明function foo(){}
另一种是通过函数表达式var foo = function(){} ,那这两种在函数提升有什么区别呢?
console.log(f1); // function f1(){}
function f1() {} // 函数声明
console.log(f2); // undefined
var f2 = function() {}; // 函数表达式
接下来我们通过一个例子来说明这个问题:
function test() {
foo(); // Uncaught TypeError "foo is not a function"
bar(); // "this will run!"
var foo = function() {
// function expression assigned to local variable 'foo'
alert("this won't run!");
};
function bar() {
// function declaration, given the name 'bar'
alert("this will run!");
}
}
test();
在上面的例子中,foo()调用的时候报错了,而 bar 能够正常调用。
我们前面说过变量和函数都会上升,遇到函数表达式 var foo = function(){}时,首先会将var foo上升到函数体顶部,然而此时的 foo 的值为 undefined,所以执行foo()报错。
而对于函数bar(), 则是提升了整个函数,所以bar()才能够顺利执行。
有个细节必须注意:当遇到函数和变量同名且都会被提升的情况,函数声明优先级比较高,因此变量声明会被函数声明所覆盖,但是可以重新赋值。
alert(a); //输出:function a(){ alert('我是函数') }
function a() {
alert("我是函数");
} //
var a = "我是变量";
alert(a); //输出:'我是变量'
function 声明的优先级比 var 声明高,也就意味着当两个同名变量同时被 function 和 var 声明时,function 声明会覆盖 var 声明
这代码等效于:
function a() {
alert("我是函数");
}
var a; //hoisting
alert(a); //输出:function a(){ alert('我是函数') }
a = "我是变量"; //赋值
alert(a); //输出:'我是变量'
最后我们看个复杂点的例子:
function test(arg) {
// 1. 形参 arg 是 "hi"
// 2. 因为函数声明比变量声明优先级高,所以此时 arg 是 function
console.log(arg);
var arg = "hello"; // 3.var arg 变量声明被忽略, arg = 'hello'被执行
function arg() {
console.log("hello world");
}
console.log(arg);
}
test("hi");
/* 输出:
function arg(){
console.log('hello world')
}
hello
*/
这是因为当函数执行的时候,首先会形成一个新的私有的作用域,然后依次按照如下的步骤执行:
如果有形参,先给形参赋值
进行私有作用域中的预解释,函数声明优先级比变量声明高,最后后者会被前者所覆盖,但是可以重新赋值
私有作用域中的代码从上到下执行
有个细节必须注意:当遇到函数和变量同名且都会被提升的情况,函数声明优先级比较高,因此变量声明会被函数声明所覆盖,但是可以重新赋值。
当函数执行的时候,首先会形成一个新的私有的作用域,然后依次按照如下的步骤执行:
- 如果有形参,先给形参赋值
- 进行私有作用域中的预解释,函数声明优先级比变量声明高,最后后者会被前者所覆盖,但是可以重新赋值
- 私有作用域中的代码从上到下执行
3. 确定 this 的指向
先搞明白一个很重要的概念 —— this 的值是在执行的时候才能确认,定义的时候不能确认! 为什么呢 —— 因为 this 是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。看如下例子: ```jsx // 情况1 function foo() { console.log(this.a) //1 } var a = 1 foo()
// 情况2 function fn(){ console.log(this); } var obj={fn:fn}; obj.fn(); //this->obj
// 情况3 function CreateJsPerson(name,age){ //this是当前类的一个实例p1 this.name=name; //=>p1.name=name this.age=age; //=>p1.age=age } var p1=new CreateJsPerson(“尹华芝”,48);
// 情况4 function add(c, d){ return this.a + this.b + c + d; } var o = {a:1, b:3}; add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16 add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34
// 情况5
接下来我们逐一解释上面几种情况
- 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
- 对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
- 在构造函数模式中,类中(函数体中)出现的 this.xxx=xxx 中的 this 是当前类的一个实例
- call、apply 和 bind:this 是第一个参数
- 箭头函数 this 指向:箭头函数没有自己的 this,看其外层的是否有函数,如果有,外层函数的 this 就是内部箭头函数的 this,如果没有,则 this 是 window。
![image.png](https://cdn.nlark.com/yuque/0/2022/png/10384213/1649411240075-07eb9378-a74c-4386-9225-54878c9b5b14.png#clientId=u51b39270-357d-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u3007e46e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=426&originWidth=602&originalType=url&ratio=1&rotation=0&showTitle=false&size=57847&status=done&style=none&taskId=u882f17da-b73c-4c90-9ba0-5f0ba0e789b&title=)
<a name="RpV7C"></a>
### 四、执行上下文栈(Execution Context Stack)
函数多了,就有多个函数执行上下文,每次调用函数创建一个新的执行上下文,那如何管理创建的那么多执行上下文呢?<br />JavaScript 引擎创建了执行上下文栈来管理执行上下文。**可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则**。<br />![](https://cdn.nlark.com/yuque/0/2022/gif/10384213/1649411240194-16281321-0478-463b-b01b-1884b3d30aed.gif#clientId=u51b39270-357d-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ud6040061&margin=%5Bobject%20Object%5D&originHeight=238&originWidth=486&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=uce6c0838-7a3e-4d53-aedf-4d8f245dfe6&title=)<br />从上面的流程图,我们需要记住几个关键点:
- JavaScript 执行在单线程上,所有的代码都是排队执行。
- 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
- 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
- 浏览器的 JS 执行引擎总是访问栈顶的执行上下文。
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈。
我们再来看个例子:
```jsx
var color = "blue";
function changeColor() {
var anotherColor = "red";
function swapColors() {
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
}
swapColors();
}
changeColor();
上述代码运行按照如下步骤:
- 当上述代码在浏览器中加载时,JavaScript 引擎会创建一个全局执行上下文并且将它推入当前的执行栈
- 调用 changeColor 函数时,此时 changeColor 函数内部代码还未执行,js 执行引擎立即创建一个 changeColor 的执行上下文(简称 EC),然后把这执行上下文压入到执行栈(简称 ECStack)中。
- 执行 changeColor 函数过程中,调用 swapColors 函数,同样地,swapColors 函数执行之前也创建了一个 swapColors 的执行上下文,并压入到执行栈中。
- swapColors 函数执行完成,swapColors 函数的执行上下文出栈,并且被销毁。
- changeColor 函数执行完成,changeColor 函数的执行上下文出栈,并且被销毁。
原文:https://www.cnblogs.com/fundebug/p/understand-javascript-context-and-stack.html
4.this的原理以及几种不同使用场景的取值
原文:https://www.jb51.net/article/180383.htm
5.闭包的实现原理和作用,可以列举几个开发中闭包的实际应用
认识闭包:
满足以下特点就是闭包:
1、函数嵌套函数
2、内部函数引用外部函数的变量或者形参
3、被引用的变量或者形参可以常驻内存。(不会被垃圾回收机制所回收)
垃圾回收机制。
好处:
1、希望一个变量常驻在内存当中
2、避免全局变量污染 (首要好处)
3、如按钮下标,循环索引等问题,用闭包实现,可以解决按钮点击下标的问题
4、可以进行变量和函数的私有化
坏处:
变量常驻在内存当中,造成内存泄漏。 对于一些比较占内存的数据,要尽量避免使用闭包
闭包的实现原理和作用
1、闭包的概念:指有权访问另一个函数作用域中的变量的函数,一般情况就是在一个函数中包含另一个函数。
2、闭包的作用:访问函数内部变量、保持函数在环境中一直存在,不会被垃圾回收机制处理
因为函数内部声明 的变量是局部的,只能在函数内部访问到,但是函数外部的变量是对函数内部可见的,这就是作用域链的特点了。
子级可以向父级查找变量,逐级查找,找到为止
因此我们可以在函数内部再创建一个函数,这样对内部的函数来说,外层函数的变量都是可见的,然后我们就可以访问到他的变量了。
3、闭包的优点:
方便调用上下文中声明的局部变量
逻辑紧密,可以在一个函数中再创建个函数,避免了传参的问题
4、闭包的缺点:
因为使用闭包,可以使函数在执行完后不被销毁,保留在内存中,如果大量使用闭包就会造成内存泄露,内存消耗很大
实际开发中JS闭包的应用
1。在函数外使用函数内的变量 .函数作为返回值 (闭包作用:避免变量被环境污染)
2.函数作为参数传递
3.将函数与其所操作的某些数据关联起来,通常,你使用只有一个方法的对象的地方,都可以使用闭包
4.用闭包模拟私有方法
//这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。
var makeCounter = function () { var privateCounter = 0; function changeBy(val){ privateCounter += val; }; return { increment: function(){ changeBy(1); }, decrement: function(){ changeBy(-1); }, value: function(){ return privateCounter; } } }; var Counter1 = makeCounter(); var Counter2 = makeCounter(); Counter1.increment(); console.log(Counter1.value());//1 每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。 console.log(Counter2.value());//0 以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。
5.循环里面的闭包
怎么才能实现输出0-5呢?
原文链接:https://blog.csdn.net/binbin_1989/article/details/107064182
6.理解堆栈溢出和内存泄漏的原理,如何防止
堆栈溢出和内存泄漏的原理
1、内存泄露:是指申请的内存执行完后没有及时的清理或者销毁,占用空闲内存,内存泄露过多的话,就会导致后面的程序申请不到内存。因此内存泄露会导致内部内存溢出
2、堆栈溢出:是指内存空间已经被申请完,没有足够的内存提供了
3、在一些编程软件中,比如c语言中,需要使用malloc来申请内存空间,再使用free释放掉,需要手动清除。而js中是有自己的垃圾回收机制的,一般常用的垃圾收集方法就是标记清除。
标记清除法:在一个变量进入执行环境后就给它添加一个标记:进入环境,进入环境的变量不会被释放,因为只要“执行流”进入响应的环境,就可能用到他们。当变量离开环境后,则将其标记为“离开环境”。
4、常见的内存泄露的原因:
- 全局变量引起的内存泄露
- 闭包
- 没有被清除的计时器
5、解决方法:
- 减少不必要的全局变量
- 减少闭包的使用(因为闭包会导致内存泄露)
- 避免死循环的发生
原文地址:https://www.bbsmax.com/A/Vx5MZYQpdN/
7.如何处理循环的异步操作
一次性做完所有异步
1. async/await
const arr = [1, 2, 3, 4, 5]
const timeOut = (time) => {
return new Promise(resolve => {
console.log(resolve,time);
setTimeout(resolve, time);
})
}
(async () => {
for await (let i of arr.map(t => {
return timeOut(t * 1000).then(
() => console.log(t)
)
}));
})()
执行效果:
异步输出,1秒后输出1,2秒后输出2,3秒后输出3.。。。
2.利用Promise.all
执行所有的异步
const arr = [1, 2, 3, 4, 5]
const timeOut = (time) => {
return new Promise(resolve => {
console.log(resolve,time);
setTimeout(resolve, time);
})
}
const a = arr.map(t => {
return timeOut( t * 1000 ).then(
() => console.log(t)
)
})
Promise.all(a).then(() => {console.log('异步都执行完了');} )
Promise.race(a).then(() => {console.log('最快的异步执行完了,我就执行');} )
console.log(a); // [Promise, Promise, Promise, Promise, Promise]
执行效果:
异步输出,1秒后输出1,2秒后输出2,3秒后输出3.。。。
一次只做一个异步操作
1. async/await
const arr = [1, 2, 3, 4, 5]
const timeOut = (time) => {
return new Promise(resolve => {
console.log(resolve,time);
setTimeout(resolve, time);
})
}
(async () => {
for ( let i = 0; i < arr.length; i++ ) {
await timeOut( arr[i] * 1000 )
.then(
() => console.log(arr[i])
// 这里要是函数,timeOut函数里使用resolve作为setTimeout的第一个参数,.then里的函数体会被作为promise的第一个参数
)
}
})()
执行效果:
异步输出,间隔1秒后输出1,间隔2秒后输出2,3秒后输出3.。。。
2.利用Array.reduce
console.time("promise one by one");
arr.reduce((accumulator, currentValue, currentIndex) => {
return accumulator.then(print(`${arr[currentIndex]}-Array.reduce`)).then(() => {
if (currentIndex === arr.length - 1) {
return doAsync(currentValue * 100).then(() => {
console.timeEnd("promise one by one");
});
}
return doAsync(currentValue * 100);
});
}, doAsync(arr[0] * 100));
//吐槽 着实麻烦--没看
1.map循环的异步处理
如果在map中使用promise或await, map 始终返回promise数组,这是因为异步函数总是返回promise。
const arr = [1, 2, 3, 4, 5]
// 异步函数
const timeOut = (time) => {
return new Promise(resolve => {
console.log(resolve,time);
setTimeout(resolve, time);
})
}
const mapF = async _ => {
console.log('start');
const mapResult = await arr.map( async t => {
return await timeOut(t * 1000)
.then(() => {
console.log(t); return t
})
})
console.log(mapResult);
console.log('end');
}
mapF()
执行效果:
start
[Promise, Promise, Promise, Promise, Promise]
end
1
2
3
4
5
Promise.all(mapF).then(() => {console.log('异步都完了,但有一个异常,我就停止了');} )
Promise.allSettled(mapF).then(() => {console.log('异步都执行完了');} )
Promise.race(mapF).then(() => {console.log('最快的异步执行完了,我就执行');} )
如果你在 map 中使用 await,map 总是返回promises,你必须等待promises 数组得到处理。 或者通过await Promise.all(arrayOfPromises)/Promise.allSettled()来完成此操作。
注意:Promise.allSettled(): 它和promise.all的区别在于,all有短路操作,当异步promise数组中有一个异常,后续不会执行其他操作。而allSettled不管有没有异常,只要promise完成了(状态时fulfilled/Resolved),就继续
2.for循环的异步处理
在for循环中,使用timeOut来模拟异步操作,并将数量打印到控制台。
由于timeOut返回一个promise,我们使用 await 来等待结果的返回并打印它。
// 异步函数
const timeOut = (time) => {
return new Promise(resolve => {
setTimeout(()=> {}, time);
})
}
const forF = async () => {
console.log('start');
for (let index = 0; index < arr.length; index++) {
const item = arr[index];
const numFruit = await timeOut(item).(() => item);
console.log(numFruit);
}
console.log('end');
}
执行结果:
start
1
2
3
4
5
end
总结:
如果你想连续执行await调用,请使用for循环(或任何没有回调的循环)。
永远不要和forEach一起使用await,而是使用for循环(或任何没有回调的循环)。
不要在 filter 和 reduce 中使用 await,如果需要,先用 map 进一步骤处理,然后在使用 filter 和 reduce进行处理。
原文地址:https://juejin.cn/post/6844904128276070414
循环里正确使用await进行异步操作问题
8.理解模块化解决的实际问题,可列举几个模块化方案并理解其中原理
(todo)
1.CommonJS : nodejs中遵守的就是commonjs规范
Javascript模块化编程(一):模块的写法
2.AMD
Javascript模块化编程(二):AMD规范
Javascript模块化编程(三):require.js的用法
3.CMD
4.ES6 (应用广泛)import