作用域和闭包

304 阅读10分钟

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

  • 作用域:
    定义:作用域是指程序源代码中定义变量的区域。
    作用:作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
    在javaScript中的应用 :JavaScript采用词法作用域(lexical scoping),也就是静态作用域。
  • 静态作用域与动态作用域:
    因为javaScript采用的是词法作用域,函数的作用域在函数定义的时候就决定了。
    而词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
  • 动态作用域
    2.理解JavaScript的作用域和作用域链
    和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的,最常见的例如函数内部需要注意的是,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!
    只要函数内定义了一个局部变量,函数在解析的时候都会将这个变量“提前声明”:
    2.2作用域链(Scope Chain) 根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问
    2.3执行环境(execution context)
    js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。
    全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。
    js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。
  • 闭包:
    1.可以读取自身函数外部的变量(沿着作用域链寻找)
    2.让这些外部变量始终保存在内存中
  • 关于this对象
    this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象调用时,this等于那个对象。不过,匿名函数具有全局性,因此this对象同常指向window
  • 3.理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题
    3.1、什么是执行上下文:
    简而言之,执行上下文就是当前JavaScript代码被解析和执行时所在环境的抽象概念,JavaScript中运行任何的代码都是在执行上下文中运行。
    3.2、执行上下文类型:
     1)全局执行上下文,这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。
     1. 创建一个全局对象,在浏览器中这个对象就是window对象。
     2. 将this指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
    2)函数执行上下文:
    每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用时才会被创建,一个程序中可以存在任意数量的函数执行上下文,每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤。
    3)Eval函数执行上下文
    运行在eval函数中的代码也获得了自己的执行上下文,但是JavaScript中不常用eval函数,这里不再详细叙述。
    3.3、执行上下文生命周期:
    执行上下文的生命周期包括三个阶段:创建阶段 -> 执行阶段 -> 回收阶段
     1)创建阶段:当函数被调用,但未执行任何其内部代码之前,会做三件事:
     ① 创建变量对象:首先初始化函数的参数 arguments ,提升函数声明和变量声明。
     ② 创建作用域链:在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一级的父作用域中查找,直到找到该变量。
     ③ 确定 this 指向:包含多种情况,下文详细叙述。
    在一段JS脚本执行之前,会先解析代码(所以说JS是解释执行的脚本语言),解析的时候会先创建一个全局的执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来。变量先赋值为undefined,函数则先声明好可使用,这一步做完了,然后开始正式执行程序。
    另外一个函数在执行之前会先创建一个函数执行上下文环境,跟全局上下文差不多,不过函数执行上下文中会出现 this arguments 和函数的参数。
     2)执行阶段: 执行变量赋值、代码执行
    3)回收阶段:执行上下文出栈等虚拟机回收执行上下文
    3.4、变量提升和this指向细节
    1)变量名提升:
    2)函数声明提升:
    前面说变量名和函数都会上升,而遇到函数表达式var foo = function(){}时,首先会将var foo上升到函数体顶部,而此时的foo的值为undefined,所以执行foo()报错。
    而对于函数bar(), 则是提升了整个函数,所以bar()才能够顺利执行。
    有个细节必须注意:当遇到函数和变量同名且都会被提升的情况,函数声明优先级比较高,因此变量声明会被函数声明所覆盖,但是可以重新赋值。
     ① 如果有形参,先给形参赋值
     ② 进行私有作用域中的预解释,函数声明优先级比变量声明高,最后后者会被前者所覆盖,但是可以重新赋值
     ③ 私有作用域中的代码从上到下执行
    3)确定this的指向:
    this 的值是在执行的时候才能确认,定义的时候不能确认。因为this是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候.
    4)执行上下文栈:可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。
     1、当上述代码在浏览器中加载时,JavaScript 引擎会创建一个全局执行上下文并且将它推入当前的执行栈
    2、调用 changeColor 函数时,此时 changeColor 函数内部代码还未执行,js 执行引擎立即创建一个 changeColor 的执行上下文(简称 EC),然后把这执行上下文压入到执行栈(简称 ECStack)中。
    3、执行 changeColor 函数过程中,调用 swapColors 函数,同样地,swapColors 函数执行之前也创建了一个 swapColors 的执行上下文,并压入到执行栈中。
    4、swapColors 函数执行完成,swapColors 函数的执行上下文出栈,并且被销毁。
    5、changeColor 函数执行完成,changeColor 函数的执行上下文出栈,并且被销毁。
    4.闭包的实现原理和作用,可以列举几个开发中闭包的实际应用
    4.1、闭包的概念:指有权访问另一个函数作用域中的变量的函数,一般情况就是在一个函数中包含另一个函数。
    4.2、闭包的作用:访问函数内部变量、保持函数在环境中一直存在,不会被垃圾回收机制处理
    foo()包含bar()内部作用域的闭包,使得该作用域能够一直存活,不会被垃圾回收机制处理掉,这就是闭包的作用,以供foo()在任何时间进行引用。
    4.3、闭包的优点:
    方便调用上下文中声明的局部变量
    逻辑紧密,可以在一个函数中再创建个函数,避免了传参的问题
    4.4、闭包的缺点:
    因为使用闭包,可以使函数在执行完后不被销毁,保留在内存中,如果大量使用闭包就会造成内存泄露,内存消耗很大
    4.5、闭包在实际中的应用
    一般setTimeout的第一个参数是个函数,但是不能传值。如果想传值进去,可以调用一个函数返回一个内部函数的调用,将内部函数的调用传给setTimeout。内部函数执行所需的参数,外部函数传给他,在setTimeout函数中也可以访问到外部函数。
    5.this的原理以及几种不同使用场景的取值
  • this 是什么
    理解this之前, 先纠正一个观点,this 既不指向函数自身,也不指函数的词法作用域。如果仅通过this的英文解释,太容易产生误导了。它实际是在函数被调用时才发生的绑定,也就是说this具体指向什么,取决于你是怎么调用的函数。
  • this 的四种绑定规则:
    默认绑定、隐式绑定、显示绑定、new 绑定。优先级从低到高。
  • 规则例外
    在显示绑定中,对于null和undefined的绑定将不会生效。

    1. 这里传null的一种具体使用场景是函数柯里化的使用
  • 扩展:箭头函数
    介绍一下ES6中的箭头函数。通过“=>”而不是function创建的函数,叫做箭头函数。它的this绑定取决于外层(函数或全局)作用域。
    箭头函数的this绑定只取决于外层(函数或全局)的作用域,对于前面的4种绑定规则是不会生效的。它也是作为this机制的一种替换,解决之前this绑定过程各种规则带来的复杂性。
    6.理解堆栈溢出和内存泄漏的原理,如何防止
    1、内存泄露:是指申请的内存执行完后没有及时的清理或者销毁,占用空闲内存,内存泄露过多的话,就会导致后面的程序申请不到内存。因此内存泄露会导致内部内存溢出
    2、堆栈溢出:是指内存空间已经被申请完,没有足够的内存提供了
    3、在一些编程软件中,比如c语言中,需要使用malloc来申请内存空间,再使用free释放掉,需要手动清除。而js中是有自己的垃圾回收机制的,一般常用的垃圾收集方法就是标记清除。
    标记清除法:在一个变量进入执行环境后就给它添加一个标记:进入环境,进入环境的变量不会被释放,因为只要执行流进入响应的环境,就可能用到他们。当变量离开环境后,则将其标记为“离开环境”。
    4、常见的内存泄露的原因
    全局变量引起的内存泄露
    闭包
    没有被清除的计时器
    5、解决方法
    减少不必要的全局变量
    减少闭包的使用(因为闭包会导致内存泄露)
    避免死循环的发生