1.理解词法作用域和动态作用域
2.理解JavaScript的作用域和作用域链
3.理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题
4.this的原理以及几种不同使用场景的取值
5.闭包的实现原理和作用,可以列举几个开发中闭包的实际应用
6.理解堆栈溢出和内存泄漏的原理,如何防止
7.如何处理循环的异步操作
1.理解词法作用域和动态作用域
词法作用域,也叫静态作用域,它的作用域是指在词法分析阶段就确定了,不会改变。动态作用域是在运行时根据程序的流程信息来动态确定的,而不是写代码时进行静态确定的。
需要明确的是,Javascript并不具有动态作用域,它只有词法作用域,简单明了。但是,它的 eval()
、with
、this
机制某种程度上很像动态作用域,使用上要特别注意。
2.理解JavaScript的作用域和作用域链
作用域是在运行代码中的某些特定部分中的变量,函数和对象的可访问性。作用于决定了代码区块中变量和其他资源的可见性。作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了‘块级作用域’,可通过新增命令 let 和 const 来体现。
当代码在一个环境中执行时,会创建变量对象的一个作用域链。由子级作用域返回父级作用域中寻找变量,就叫做作用域链。作用域链是保证执行环境有权访问的所有变量和函数的有序访问。
延长作用域链:
执行环境的类型只有两种,全局和局部(函数)。但是有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。
具体来说就是执行这两个语句时,作用域链都会得到加强。
1、try - catch 语句的catch块;会创建一个新的变量对象,包含的是被抛出的错误对象的声明。
2、with 语句。with 语句会将指定的对象添加到作用域链中
3.理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题
执行上下文是评估和执行JavaScript代码的环境的抽象概念。每当JavaScript代码在运行的时候,它都是在执行上下文中运行。
执行栈,也就是其他编程语言中所说的“调用栈”,是一种拥有LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
执行上下文总共有三种类型
- 全局执行上下文
- 函数执行上下文
- Eval函数执行上下文
执行上下文的声明周期包含三个阶段创建阶段
→执行阶段
→回收阶段
创建阶段
- 创建变量对象
- 创建作用域链
- 确定this 指向
4.this的原理以及几种不同使用场景的取值
场景一:构造函数
所谓构造函数就是用来new对象的函数。其实严格来说,所有的函数都可以new一个对象,但是有些函数的定义是为了new一个对象,而有些函数则不是。另外注意,构造函数的函数名第一个字母大写(规则约定)。例如:Object、Array、Function等
function Foo(){
this.name='Apple',
this.type='fruit';
console.log(this);//Foo{name:'Apple',type:'fruit'}
}
var f1=new Foo();
console.log(f1.name,f1.type)//Apple,fruit
以上代码中,如果函数作为构造函数用,那么其中的this就代表它即将new出来的对象。
注意:
以上仅限new Foo()的情况,即Foo函数作为构造函数的情况。如果直接调用Foo函数,而不是new Foo(),情况就大不一样了。
function Foo(){
this.name='Apple',
this.type='fruit';
console.log(this);//Window{top:Window,window:Window……}
}
Foo();
这种情况下this是window。
在构造函数的prototype中,this代表什么。
function Fn(){
this.name='Apple',
this.type='fruit';
}
Fn.prototype.getName=function(){
console.log(this.name)
}
var f1=new Fn();
f1.getName();//Apple
如上代码,在Fn.prototype.getName函数中,this指向的是f1对象。因此可以通过this.name获取f1.name的值。
其实,不仅仅是构造函数的prototype,即便是在整个原型链中,this代表的也都是当前对象的值。
场景二:函数作为对象的一个属性
如果函数作为对象的一个属性时,并且作为对象的一个属性被调用时,函数中的this指向该对象。
var obj={
x:10,
fn:function(){
console.log(this);//Object{x:10,fn:function}
}
}
obj.fn();
以上代码中,fn不仅作为一个对象的一个属性,而且的确是作为对象的一个属性被调用。结果this就是obj对象。
var obj={
x:10,
fn:function(){
console.log(this); //Window{top:Window,window:Window……}
}
}
var fn1=obj.fn;
fn1();
以上代码,如果fn函数被复制到了另一个变量中,并没有作为obj的一个属性被调用,那么this的值就是window,this.x就是undefined。
场景三:函数用call或者apply调用
当一个函数被call和apply调用时,this的值就取传入的对象的值。
var obj = {
x:10
}
var fn = function(){
console.log(this); //Object{x:10}
console.log(this.x); //10
}
fn.call(obj);
场景四:全局&调用普通函数
在全局环境下,this永远是window。
console.log(this === window);//true
普通函数在调用时,其中的this也都是window。
var x= 10;
var fn = function (){
console.log(this);//Window{top:Window;window:Window……}
console.log(this.x);//10
}
fn()
下面情况要注意:
var obj = {
x:10,
fn:function(){
function f(){
console.log(this)//Window{top:Window;window:Window……}
console.log(this.x);//undefined
}
f();
}
}
obj.fn();
函数f虽然是在obj.fn内部定义的,但是它仍然是一个普通的函数,this仍然指向window。
5.闭包的实现原理和作用,可以列举几个开发中闭包的实际应用
闭包是一个拥有许多变量和绑定了这些变量的环境表达式(通常是一个函数),因而这些变量也是该表达式的一部分。换句话说,JavaScript中所有的function都是一个闭包。
一般来说,嵌套的function所产生的闭包更为强大。
function a() {
var i = 0;
function b() { alert(++i); } //函数b嵌套在函数a内部;
return b;//函数a返回函数b。
}
var c = a();
c();
这样在执行完var c=a()后,变量c实际上是指向了函数b,再执行c()后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就创建了一个闭包,为什么?因为函数a外的变量c引用了函数a内的函数b,就是说:
当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个闭包.
所谓“闭包”,就是在构造函数体内定义另外的函数作为目标对象的方法函数,而这个对象的方法函数反过来引用外层函数体中的临时变量。这使得只要目标 对象在生存期内始终能保持其方法,就能间接保持原构造函数体当时用到的临时变量值。尽管最开始的构造函数调用已经结束,临时变量的名称也都消失了,但在目 标对象的方法内却始终能引用到该变量的值,而且该值只能通这种方法来访问。即使再次调用相同的构造函数,但只会生成新对象和方法,新的临时变量只是对应新 的值,和上次那次调用的是各自独立的。
简而言之,闭包的作用就是在a执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a所占用的资源,因为a的内部函数b的执行需要依赖a中的变量。这是对闭包作用的非常直白的描述,不专业也不严谨,但大概意思就是这样,理解闭包需要循序渐进的过程。
当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)
设置为定义a时a所在的“环境”,如果a是一个全局函数,则scope chain
中只有window对象。
当执行函数a的时候,a会进入相应的执行环境(excution context)
。
在创建执行环境的过程中,首先会为a添加一个scope
属性,即a的作用域
,其值就为第1步中的scope chain。即a.scope=a的作用域链
。
然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过JavaScript代码直接访问。创建完活动对象后,把活动对象添加到a的作用域链的最顶端。此时a的作用域链包含了两个对象:a的活动对象和window对象。
下一步是在活动对象上添加一个arguments属性,它保存着调用函数a时所传递的参数。
最后把所有函数a的形参和内部的函数b的引用也添加到a的活动对象上。在这一步中,完成了函数b的的定义,因此如同第3步,函数b的作用域链被设置为b所被定义的环境,即a的作用域。
到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,又函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。
当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、a的活动对象和window对象,如下图所示:
如图所示,当在函数b中访问一个变量的时候,搜索顺序是:
先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索函数a的活动对象,依次查找,直到找到为止。
如果函数b存在prototype原型对象,则在查找完自身的活动对象后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。
如果整个作用域链上都无法找到,则返回undefined。
小结,本段中提到了两个重要的词语:函数的定义与执行。文中提到函数的作用域是在定义函数时候就已经确定,而不是在执行的时候确定。用一段代码来说明这个问题
function f(x) {
var g = function () { return x; }
return g;
}
var h = f(1);
alert(h());
这段代码中变量h指向了f中的那个匿名函数(由g返回)。
假设函数h的作用域是在执行alert(h())确定的,那么此时h的作用域链是:h的活动对象->alert的活动对象->window对象。
假设函数h的作用域是在定义时确定的,就是说h指向的那个匿名函数在定义的时候就已经确定了作用域。那么在执行的时候,h的作用域链为:h的活动对象->f的活动对象->window对象。
如果第一种假设成立,那输出值就是undefined;如果第二种假设成立,输出值则为1。
运行结果证明了第2个假设是正确的,说明函数的作用域确实是在定义这个函数的时候就已经确定了。
闭包的应用场景
保护函数内的变量安全。以最开始的例子为例,函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。
在内存中维持一个变量。依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1。
通过保护变量的安全实现JS私有属性和私有方法(不能被外部访问)
私有属性和方法在Constructor外是无法被访问的
function Constructor(...) {
var that = this;
var membername = value;
function membername(...) {...}
}
以上3点是闭包最基本的应用场景,很多经典案例都源于此。
在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。
var声明的变量由于不存在块级作用域所以可以在全局环境中调用,而let声明的变量由于存在块级作用域所以不能在全局环境中调用。
var a=[];
for(var i=0;i<10;i++){
a[i]=function(){
console.log(i);
};
}
a[6](); //10
var b=[];
for(let i=0;i<10;i++){
b[i]=function(){
console.log(i);
};
}
b[6]();//6
6.理解堆栈溢出和内存泄漏的原理,如何防止
内存泄露:指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束
JS的回收机制
JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是实时的,因为其开销比较大,所以垃圾回收系统(GC)会按照固定的时间间隔,周期性的执行。
到底哪个变量是没有用的?所以垃圾收集器必须跟踪到底哪个变量没用,对于不再有用的变量打上标记,以备将来收回其内存。用于标记的无用变量的策略可能因实现而有所区别,通常情况下有两种实现方式:标记清除
和引用计数
。引用计数不太常用,标记清除较为常用
标记清除
js中最常用的垃圾回收方式就是标记清除。当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
function test(){
var a=10;//被标记,进入环境
var b=20;//被标记,进入环境
}
test();//执行完毕之后a、b又被标记离开环境,被回收
引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值(function object array)赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。
function test(){
var a={};//a的引用次数为0
var b=a;//a的引用次数加1,为1
var c=a;//a的引用次数加1,为2
var b={};//a的引用次数减1,为1
}
哪些情况会造成内存泄露
1.意外的全局变量引起的内存泄露
function leak(){
leak="xxx";//leak成为一个全局变量,不会被回收
}
2.闭包引起的泄露
function bindEvent(){
var obj=document.createElement("XXX");
obj.οnclick=function(){
//Even if it's a empty function
}
}
闭包可以维持函数内局部变量,使其得不到释放。 上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调的引用外暴了,形成了闭包。
解决之道,将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。
//将事件处理函数定义在外部
function onclickHandler(){
//do something
}
function bindEvent(){
var obj=document.createElement("XXX");
obj.οnclick=onclickHandler;
}
//在定义事件处理函数的外部函数中,删除对dom的引用
function bindEvent(){
var obj=document.createElement("XXX");
obj.οnclick=function(){
//Even if it's a empty function
}
obj=null;
}
3.没有清理的DOM元素引用
var elements={
button: document.getElementById("button"),
image: document.getElementById("image"),
text: document.getElementById("text")
};
function doStuff(){
image.src="http://some.url/image";
button.click():
console.log(text.innerHTML)
}
function removeButton(){
document.body.removeChild(document.getElementById('button'))
}
- 被遗忘的定时器或者回调
var someResouce=getData();
setInterval(function(){
var node=document.getElementById('Node');
if(node){
node.innerHTML=JSON.stringify(someResouce)
}
},1000)
这样的代码很常见, 如果 id 为 Node 的元素从 DOM 中移除, 该定时器仍会存在, 同时, 因为回调函数中包含对 someResource 的引用, 定时器外面的 someResource 也不会被释放。
5.子元素存在引起的内存泄露
黄色是指直接被 js变量所引用,在内存里,红色是指间接被 js变量所引用,如上图,refB 被 refA 间接引用,导致即使 refB 变量被清空,也是不会被回收的子元素 refB 由于 parentNode 的间接引用,只要它不被删除,它所有的父元素(图中红色部分)都不会被删除。
怎样避免内存泄露
1)减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收;
2)注意程序逻辑,避免“死循环”之类的 ;
3)避免创建过多的对象 原则:不用了的东西要及时归还。
7.如何处理循环的异步操作
1.不需要等待结果的异步循环
async function processArray(array) {
array.forEach(async (item) => {
await func(item);
})
console.log('Done!');
}
function delay() {
return new Promise(resolve => setTimeout(resolve, 300));
}
async function delayedLog(item) {
// notice that we can await a function
// that returns a promise
await delay();
console.log(item);
}
async function processArray(array) {
array.forEach(async (item) => {
await delayedLog(item);
})
console.log('Done!');
}
processArray([1, 2, 3]);
结果输出为
Done!
1
2
3
如果不需要等结果这样写是ok的,但是在大多数案例里这不是个很好的逻辑。
2.线性处理数组
要等待结果,我们应该返回到老式的 for 循环,但这一次为了更好的可读性我们可以使用现代写法 for..of
。
sync function processArray(array) {
for (const item of array) {
await delayedLog(item);
}
console.log('Done!');
}
结果输出:
1
2
3
Done!
该代码将依次处理每一项。但是我们可以使用并行运行。
3.并行处理数组
async function processArray(array) {
// map array to promises
const promises = array.map(delayedLog);
// wait until all promises are resolved
await Promise.all(promises);
console.log('Done!');
}
这段代码将并行运行许多delayLog 任务。但是对于非常大的数组要小心(并行的任务太多对CPU或内存来说可能比较吃力)。
也不要混淆“并行”与真正的线程和并行。该代码不能保证真正的并行执行。这取决于您的 item函数(在本演示中是delayedLog)。网络请求、webworker 和其他一些任务可以并行执行。
参考链接:
https://www.jianshu.com/p/70b38c7ab69c
https://www.jianshu.com/p/2c3c8890dff0
http://www.frontopen.com/1702.html
https://www.cnblogs.com/wangfupeng1988/p/3988422.html
https://blog.csdn.net/michael8512/article/details/77888000
https://www.jianshu.com/p/9dd1014f7f1c