一、作用域

作用域是可访问变量的集合

在 JavaScript 中, 对象和函数同样也是变量。
在 JavaScript 中, 作用域为可访问变量,对象,函数的集合。

JavaScript 函数作用域: 作用域在函数内修改。

常见的作用域分为:

  • 全局作用域(window、global)
  • 函数作用域(function)
  • 块级作用域({})
  • 词法作用域(this)

函数作用域

变量在函数内声明,变量为局部作用域,也称为函数作用域。
局部变量:只能在函数内部访问。

举个例子:

function fn() {
    let a = 1
}
fn()
console.log(a) // 报错 a not defined

由此可看出,在全局访问这个变量a时,会报错,说明全局是无法获取到(闭包除外)函数内部的变量。
因为局部变量只作用于函数内,所以不同的函数可以使用相同名称的变量。
局部变量在函数开始执行时创建,函数执行完后局部变量会自动销毁。

如果想读取函数内的变量,必须借助 return 或者闭包

function fn() {
   var b = 2
    return b
}
fn()
console.log(b)
VM879:6 2

全局作用域

变量在函数外定义,即为全局变量。
全局变量有 全局作用域: 网页中所有脚本和函数均可使用。

let a = 1
function fn() {
    console.log(a)
}
fn()
console.log(a) // 打印1 1

// 函数内修改变量a,因为变量a是全局的,因此都是打印2 2
let a = 1
function fn() {
    a = 2
    console.log(a)
}
fn()
console.log(a) // 2 2

如果变量在函数内没有声明(没有使用 var 关键字),该变量为全局变量。

function fn() {
    a = 2
    console.log(a)
}
fn()
console.log(a) // 2 2

*注意:在函数内部没有声明的变量是作为window、global存在的,拥有全局作用域,但是这个变量是可以被delete的,而全局作用域不可以

块级作用域

ES6引入了let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中,在大括号之外不能访问这些变量。

{
    let t1 = 1
    const t2 = 2
    var t3 = 3
    console.log(t1, t2, t3)
}
console.log(t1, t2, t3) // t1 t2 报错 t3打印3

词法作用域

词法作用域,又叫静态作用域,变量被创建时就确定好了,而非执行阶段确定的。

只能在执行阶段才能决定变量的作用域,那就是动态作用域

var a = 12;
function foo(){
    console.log(a) // 12
}
function bar(){
    var a = 13;
    foo();
}
bar()

由于JavaScript遵循词法(静态)作用域,相同层级的 foo 和 bar 就没有办法访问到彼此块作用域中的变量,所以foo执行时没有找到变量a,会向上级寻找,foo的外层是window,因此找到了a = 12,因此输出12。

二、作用域链

js当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域,如果全局作用域还查不到改变量,则在全局范围内隐式声明该变量(非严格模式下)或是直接报错。

就像上面那个例子,稍微修改一下变量,取值就不一样

var a = 12;
function foo(){
    console.log(this.a) // 13
}
function bar(){
    this.a = 13;
    foo();
}
bar()

具体分析:

  • foo函数在内部找不到变量a,this的指向又是在b1调用的时候确定的,向上一层作用域bar函数内部找
  • 函数foo中的this指向是函数bar,在函数bar找到了变量a,因此this.a就是13
  • 如果函数bar中找不到变量a,向上一层作用域找,即全局作用域,还是找不到则报错

三、闭包

介绍

闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁

作用

一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

可以看看阮一峰的文章例子

function f1(){
  var n=999;
  nAdd=function(){n+=1}
  function f2(){
    alert(n);
  }

  return f2;
}

var result=f1();
result(); // 999

nAdd();

result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

使用注意点

(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

思考题

可以想想这两段代码分别输出什么值。

代码一:

var name = "The Window";

var object = {
  name : "My Object",

  getNameFunc : function(){
    return function(){
      return this.name;
    };

  }
};

console.log(object.getNameFunc()());

代码二:

var name = "The Window";

var object = {
  name : "My Object",

  getNameFunc : function(){
    var self = this
    return function(){
      return self.name;
    };

  }
};

console.log(object.getNameFunc()());

分析:
由上图可看出,函数getNameFunc内部打印的this指向object对象,但是在函数getNameFunc中又return一个匿名函数,且该函数的this指向为window,因此直接调用函数getNameFunc会直接打印全局变量 "The Window"。但是如果使用self.name打印时,会输出"My Object"。

当使用object.getNameFunc的方式不同时,输出的内容也不一样

var name = "The Window";

var object = {
  name : "My Object",

  getNameFunc : function(){
    console.log(this)
    var self = this
    return function(){
      console.log(this)
      return self.name;
    };

  }
};

console.log(object.getNameFunc()()) // 'My Object'

var jj = object.getNameFunc()()
jj // 'My Object'

var kk = object.getNameFunc()
kk() // 'My Object'

var ll = object.getNameFunc
ll()() // 'The Window'

图解


羽玉
10 声望0 粉丝