JavaScript的作用域

JavaScript 的作用域有两种

  • 全局作用域
  • 函数作用域

与其他语言不同的是,JavaScript 没有块级作用域,参考以下程序

  1. if (true) {
  2. var x = 1;
  3. }
  4. console.log(x); // 1

上面的变量 x 定义在 if 的语句块中,在其他的语言中,x 在语句块外是访问不到的,但是由于 JavaScript 没有块级作用域,所以变量 x 是全局变量,所以在语句块外也可以被访问到。

注意:

上面的讨论是基于 ES5 的,在 ES6 中,通过关键字 letconst 声明的变量,可以实现块级作用域的效果

  1. if (true) {
  2. let x = 1;
  3. }
  4. console.log(x); // not defined

JavaScript 作用域链

JavaScript 作用域链指的是查找变量的顺序,比如下面的程序

  1. var i = 1;
  2. function foo() {
  3. var i = 2;
  4. function bar() {
  5. var i = 3;
  6. console.log(i);
  7. }
  8. bar();
  9. }
  10. foo(); // 3

在上面的程序中,我们在 bar() 函数中访问了变量 i,这时 bar() 会先在 bar() 函数内部查找是否有变量 i,如果没有,则会去 bar() 函数的上一级作用域即 foo() 函数中寻找,如果还没有,则会去全局作用域寻找,如果没有找到,则会报错变量未定义,所以上述 bar() 函数的作用域链为

  1. bar --> foo --> 全局作用域

编译器会根据作用域链去寻找变量,如果没有找到则会报错。

闭包介绍

什么是闭包,简单的说就是允许函数访问并操作函数外部的变量,只要该变量处于该函数的作用域链中,比如

  1. function foo() {
  2. var x = 1;
  3. return function() {
  4. console.log(x);
  5. }
  6. }
  7. var func = foo();
  8. func(); // 1

函数 foo() 返回了一个函数,返回的这个函数中访问了变量 x,根据我们的讲解,会根据这个函数的作用域链去寻找这个变量 x,该匿名函数定义时的作用域链

  1. 返回的匿名函数 --> foo --> 全局作用域

所以当执行该匿名函数时,会根据上述的作用域链去寻找变量 x,会在 foo 中找到变量 x,所以输出的结果是 1

你可能会有这样的疑惑,当执行完函数 foo() 后,变量 x 不是应该已经被销毁了吗,为什么还能够被访问。之所以会有这样的想法,可能是受其他编程语言的影响,如 CJava 等,但是要明白 JavaScript 与这些编程语言不同,至少 Java 不能够返回函数,因为返回的函数还保存着对变量 x 的引用,所以变量 x 在执行完 foo() 之后是不会被清除的,这就是还能够访问 x 的原因。

那接下来看一个例子,看看你是否明白了闭包

  1. for (var i = 0; i < 10; i++) {
  2. setTimeout(function(){
  3. console.log(i);
  4. }, i * 100)
  5. }

上面的程序的效果是延时 i * 100 ms 打印输出 i,你可能希望得到下面这样的输出

  1. 0
  2. 1
  3. 2
  4. 3
  5. 4
  6. 5
  7. 6
  8. 7
  9. 8
  10. 9

但是真正的结果是

  1. 10
  2. 10
  3. 10
  4. 10
  5. 10
  6. 10
  7. 10
  8. 10
  9. 10
  10. 10

这是因为 setTimeout 是一个异步函数,当执行 setTimeout 时,并不会立即执行传入的回调函数,这些回调函数等到延迟时间到了以后,会将这些回调函数放入事件队列中,简单来说,当执行到 setTimeout 函数时,不会有任何的阻碍直接进入下一轮循环,等到循环执行完毕,编译器会取出事件队列中的函数执行(这些回调函数并不是执行到 setTimeout 方法时立即被添加到事件队列中的,而是等到设定的延迟时间后再添加到事件队列中的),所以当执行这些回调函数时,循环已经执行完毕,变量 i 的值已经变为了 10,这些回调函数根据它的作用域链找到的变量 i 的值就全部是 10 了。

闭包实现私有变量

JavaScript 中是没有关键字去声明私有变量的,但是我们可以通过闭包来实现这样的效果,如下

  1. function Person () {
  2. var name = 'ninja';
  3. this.setName = function (value) {
  4. name = value;
  5. }
  6. this.getName = function () {
  7. return name;
  8. }
  9. }
  10. var person = new Person();
  11. console.log(person.name); // 访问不到 undefined
  12. console.log(person.getName()); // ninja
  13. person.setName('dummy');
  14. console.log(person.getName()); // dummy

通过闭包,setNamegetName 可以对 name 进行访问和操作,但是却不能够被实例变量 person 访问到,因为 name 并不是 person 的属性,这样我们就实现了私有变量。

闭包处理回调函数

假设有这么一个动画函数

  1. var tick = 0;
  2. function animateIt(id) {
  3. var element = document.getElementById(id);
  4. var timer = setInterval(function () {
  5. if (tick < 100) {
  6. element.style.left = element.style.top = tick + "px";
  7. tick++;
  8. } else {
  9. clearInterval(timer);
  10. }
  11. }, 10);
  12. }

该函数实现在 1s 将元素向下和向右平移 100px,如下

  1. document.getElementById("box1").addEventListener('click', function () {
  2. animateIt("box1");
  3. })

JavaScript闭包 - 图1
但是当我们同时对两个元素使用动画时,由于二者共享变量 tick,则会导致二者的动画状态发生冲突,所以我们改动如下

  1. function animateIt(id) {
  2. var tick = 0;
  3. var element = document.getElementById(id);
  4. var timer = setInterval(function () {
  5. if (tick < 100) {
  6. element.style.left = element.style.top = tick + "px";
  7. tick++;
  8. } else {
  9. clearInterval(timer);
  10. }
  11. }, 10);
  12. }
  13. document.getElementById("box1").addEventListener('click', function () {
  14. animateIt("box1");
  15. })
  16. document.getElementById("box2").addEventListener('click', function () {
  17. animateIt("box2");
  18. })

我们将 tick 定义在函数内,由于闭包,setInteval 中的回调函数可以访问到tick,并且两个不同id 元素的tick是不同的,不会相互干扰
JavaScript闭包 - 图2