javascript基础知识问答-作用域和闭包

1.理解词法作用域和动态作用域
2.理解JavaScript的作用域和作用域链
3.理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题
4.this的原理以及几种不同使用场景的取值
5.闭包的实现原理和作用,可以列举几个开发中闭包的实际应用
6.理解堆栈溢出和内存泄漏的原理,如何防止
7.如何处理循环的异步操作

1.理解词法作用域和动态作用域

词法作用域,也叫静态作用域,它的作用域是指在词法分析阶段就确定了,不会改变。动态作用域是在运行时根据程序的流程信息来动态确定的,而不是写代码时进行静态确定的。

需要明确的是,Javascript并不具有动态作用域,它只有词法作用域,简单明了。但是,它的 eval()withthis机制某种程度上很像动态作用域,使用上要特别注意。

2.理解JavaScript的作用域和作用域链

作用域是在运行代码中的某些特定部分中的变量,函数和对象的可访问性。作用于决定了代码区块中变量和其他资源的可见性。作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了‘块级作用域’,可通过新增命令 let 和 const 来体现。

当代码在一个环境中执行时,会创建变量对象的一个作用域链。由子级作用域返回父级作用域中寻找变量,就叫做作用域链。作用域链是保证执行环境有权访问的所有变量和函数的有序访问。

延长作用域链:
执行环境的类型只有两种,全局和局部(函数)。但是有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。
具体来说就是执行这两个语句时,作用域链都会得到加强。
1、try - catch 语句的catch块;会创建一个新的变量对象,包含的是被抛出的错误对象的声明。
2、with 语句。with 语句会将指定的对象添加到作用域链中

3.理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题

执行上下文是评估和执行JavaScript代码的环境的抽象概念。每当JavaScript代码在运行的时候,它都是在执行上下文中运行。

执行栈,也就是其他编程语言中所说的“调用栈”,是一种拥有LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。

执行上下文总共有三种类型

  1. 全局执行上下文
  2. 函数执行上下文
  3. 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的作用链

如图所示,当在函数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个假设是正确的,说明函数的作用域确实是在定义这个函数的时候就已经确定了。

闭包的应用场景

  1. 保护函数内的变量安全。以最开始的例子为例,函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。

  2. 在内存中维持一个变量。依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1。

  3. 通过保护变量的安全实现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'))
}
  1. 被遗忘的定时器或者回调

var someResouce=getData();
setInterval(function(){
    var node=document.getElementById('Node');
    if(node){
        node.innerHTML=JSON.stringify(someResouce)
    }
},1000)

这样的代码很常见, 如果 id 为 Node 的元素从 DOM 中移除, 该定时器仍会存在, 同时, 因为回调函数中包含对 someResource 的引用, 定时器外面的 someResource 也不会被释放。

5.子元素存在引起的内存泄露


图片.png

黄色是指直接被 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

你可能感兴趣的:(javascript基础知识问答-作用域和闭包)