JavaScript的作用域

JavaScript 的作用域有两种

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

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

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

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

注意:

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

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

JavaScript 作用域链

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

var i = 1;

function foo() {
var i = 2;
function bar() {
var i = 3;
console.log(i);
}

bar();
}

foo(); // 3

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

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

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

闭包介绍

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

function foo() {
var x = 1;
return function() {
console.log(x);
}
}

var func = foo();
func(); // 1

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

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

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

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

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

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

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

0
1
2
3
4
5
6
7
8
9

但是真正的结果是

10
10
10
10
10
10
10
10
10
10

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

闭包实现私有变量

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

function Person () {
var name = 'ninja';

this.setName = function (value) {
name = value;
}

this.getName = function () {
return name;
}
}

var person = new Person();

console.log(person.name); // 访问不到 undefined

console.log(person.getName()); // ninja
person.setName('dummy');
console.log(person.getName()); // dummy

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

闭包处理回调函数

假设有这么一个动画函数

var tick = 0;
function animateIt(id) {
var element = document.getElementById(id);
var timer = setInterval(function () {
if (tick < 100) {
element.style.left = element.style.top = tick + "px";
tick++;
} else {
clearInterval(timer);
}
}, 10);
}

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

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

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

function animateIt(id) {
var tick = 0;
var element = document.getElementById(id);
var timer = setInterval(function () {
if (tick < 100) {
element.style.left = element.style.top = tick + "px";
tick++;
} else {
clearInterval(timer);
}
}, 10);
}
document.getElementById("box1").addEventListener('click', function () {
animateIt("box1");
})
document.getElementById("box2").addEventListener('click', function () {
animateIt("box2");
})

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