用简单的方式解释传说中的作用域与作用域链

今年第十号台风‘安比’来啦,外面狂风大作暴雨连连,哪敢出门啊所以就安心待在家里写一篇博客总结下自己最近学习心得,emmmmmm….那就开始吧。

作用域

坦白说日常开发过程中并不会经常关注 作用域 这个东西,而且即使程序出现了八阿哥 ( Bug ) 估计很多人也不会考虑到作用域这一块儿。但是…(此处应有强调),理解作用域对我们写出健壮、高效的代码有很大帮助。所以呢今年笔者就挑选了这个话题和大家一起探讨下。

什么叫
作用域?

这里笔者没有去查阅各种官方的解释,姑且就目前的学习和工作的心得来阐述下吧:
作用域 简单来说就是 变量的作用区域。比如我们定义一个变量 var value = 1, 那么能访问到这个变量的地方都是她的作用区域,简称 作用域。作用域可以分为 全局作用域函数作用域块作用域(ES6)。

全局作用域

没有什么比代码来得更实在,现在新建一个 index.js 文件,内容如下:

var value = 1;

console.log(`Window Area: ${value}`);   //A

function showValue() {
    console.log(`Function Area: ${value}`);  //B
}
showValue();

console.log(`Window Area: ${window.value}`);   //C

运行结果如下:

Debugger listening on ws://127.0.0.1:11698/e05297d1-af34-4244-a55f-a818c5d2951a
Debugger attached.
Window Area: 1     <----访问到了Value
Function Area: 1   <----访问到了Value
Window Area: 1     <----访问到了Value

我们在全局环境定义一个变量 value, 在 A 行和 B 行都能正确访问到,并且在C 行从 window 中正确得读取到了值。那么就可以说在window对象中变量是全局变量,她作用的作用区就是 全局作用域。而且我们都知道,ES6之前声明变量只有用 var, 所以声明一个变量就需要 var name=xxx。但如果我们创建一个变量没有用 var 会发生什么事情,通过改写刚才的代码我们来做个试验:

var originValue = 1;
newValue = 10086;
console.log(`Window Area 'Origin': ${originValue}`);   //A
console.log(`Window Area 'New': ${newValue}`);   //B

function showValue() {
    console.log(`Function Area 'Origin': ${originValue}`);  //C
    console.log(`Function Area 'New': ${newValue}`);  //D
}

showValue();

通过运行看一下结果:

Debugger listening on ws://127.0.0.1:28526/d0af124a-5020-4211-b186-bbd80e0d1403
Debugger attached.
Window Area 'Origin': 1
Window Area 'New': 10086
Function Area 'Origin': 1
Function Area 'New': 10086

emmmm…好像没有什么不同。等等….先放下手里40米的大刀,我没有在忽悠在读的朋友,请接着往下看。

函数作用域

看名字大家就能猜到函数作用域其实就是变量只在一个函数体中起作用,没毛病。但是有个例外,那就是闭包。当然闭包不是本次的讨论内容,当然会在下一篇单独拿出来和大家一起坍探讨。言归正传,我们继续来看函数作用域。

首先我们来看一个例子:

function showValue() {
    var innerValue = 10086;
    console.log(`Inner Value: ${innerValue}`);  //A
}

showValue();

console.log(`Access Inner Value: ${innerValue}`);  //B

看下运行结果,果不其然出错了:

Debugger listening on ws://127.0.0.1:15677/f3bc723c-4354-4416-87f0-25c7b9df6b64
Debugger attached.
Inner Value: 10086
ReferenceError: innerValue is not defined

在函数中能正常的访问 innerValue 这个变量,而到了函数外面就访问不到,显示 innerValue is not defined 所以这就是函数作用域的表现形式。

但再如果,我们在函数中的创建的变量没有使用 var,会发生什么呢:

function showValue() {
    innerValue = 10086;
    console.log(`Inner Value: ${innerValue}`);  //A
}

showValue();

console.log(`Access Inner Value: ${innerValue}`);  //B
console.log(`Access Inner Value: ${window.innerValue}`);  //C

重点看 C 行,我们从 window 对象中访问这个变量,看运行后浏览器控制台打印结果:

Inner Value: 10086
Access Inner Value: 10086
Access Inner Value: 10086

此时很奇怪的事情发生了,只要去掉一个 var,运行结果就截然不同了,而且我们还可以在window对象中获取到这个变量。所以我们可以得到这样一个结论:在函数体中创建变量而不用 var 这个关键字声明,那么就默认把这个变量放到 window 中,也就是作为一个全局变量,那么既然是全局变量那么其作用域也就是全局的了。所以这个例子就告诉我们,在函数中创建一个变量,一定要带上关键字( var,当然ES6还给我们提供了letconst), 初非有特殊需求,不然很容易引发各种奇怪的八阿哥。

块作用域

所谓的 块作用域 就是 某个变量只会在某一段代码中有效,一旦超出这个块那就会失效,也就是说会被当做垃圾回收了。严格来说ES6之前并没有 块作用域 ,但是可以借助 立即执行函数 人为实现, 原理就是当一个函数执行完以后其中的所有变量会被垃圾回收机制给回收掉 ( 但是也有例外,那就是闭包 )。
立即执行函数的形式很简单 (Function)(arguments),来一段代码乐呵乐呵吧:

var value = 10086;

//------------------Start---------
(function () {
    var newValue = 10001;
    value += newValue;
})()
//------------------End-----------

console.log(`value: ${value}`);
console.log(`newValue: ${newValue}`);

我们在全局环境中定义一个变量 value, 然后又在立即执行函数中定义了一个变量 newValue,将这个变量与 value 相加并重新赋值给 value 变量。运行结果如下:

Debugger listening on ws://127.0.0.1:45745/9cbc93f9-a6f0-4d31-899f-70767afcd305
Debugger attached.
value: 20087
ReferenceError: newValue is not defined

并没有如预期那样读取到 newValue 变量,原因就是她已经被回收掉了。

但是ES6对此进行了改进,只要使用 花括号{} 就可以实现一个块作用域。我们来改写下前一段代码:

let value = 10086;

{
    let newValue = 10001;
    value += newValue;
}

console.log(`value: ${value}`);
console.log(`newValue: ${newValue}`);

首先大家都能注意到我们使用 let 这个关键词声明了变量,再看运行结果:

Debugger listening on ws://127.0.0.1:44728/a37871fd-4088-4910-8b32-6f48ce78b6e6
Debugger attached.
value: 20087
ReferenceError: newValue is not defined

与前者相同。所以笔者在这里建议,开发过程中应该尽量使用 let 或者 const,这样对自己创建的变量有更好的控制。而不至于出现 作用域控制失稳(笔者意淫出来的形容词) 或者 变量覆盖

所以接下来来具体演示这两个问题:

作用域控制失稳

代码:

var functionList = [];
for (var index = 1; index < 4; index++) {
    functionList.push(function () {
        console.log(index)
    })
};

functionList.forEach(function (func) {
    func()
});

这个例子还算比较经典的例子,不理解作用域的朋友可能会认为数组中的第一个函数打印 1, 第二个函数打印 2, 第三个函数打印 3。下面我们来看下运行结果:

Debugger listening on ws://127.0.0.1:6247/d2d6f0d0-d094-4cfa-9653-b8525b43b7c0
Debugger attached.
4
4
4

打印出三个 4。这是因为var出来的变量不具有局部作用的能力,因此即使在每一次循环时候把变量 index 传给 函数,但是本质上每一个函数内部仍是index而不是每一次循环对应的数字。上面的代码等价于:

var functionList = [];
var index;
for (index = 1; index < 4; index++) {
    functionList.push(function () {
        console.log(index)
    })
};

functionList.forEach(function (func) {
    func()
});

console.log(`index: ${index}`)

看下运行结果:

Debugger listening on ws://127.0.0.1:28208/a38766b5-6baf-4341-822e-2ebefa5e8ac6
Debugger attached.
4
4
4
index: 4

所以可以意识到,index 变量已经进入了全局变量中,所以每一个函数打印的是 循环后的index

当然有两种改写方式来实现我们预想的结果,第一种是使用 立即执行函数 ,第二章是 let 关键字。下面来各自实现一下:

立即执行函数

var functionList = [];
for (var index = 1; index < 4; index++) {
    (function (index) {
        functionList.push(function () {
            console.log(index)
        })
    })(index)
};

functionList.forEach(function (func) {
    func()
});

运行结果:

Debugger listening on ws://127.0.0.1:49005/030eb056-d268-4244-a01e-1c0cf3deca24
Debugger attached.
1
2
3

let 关键字

var functionList = [];
for (let index = 1; index < 4; index++) {
    functionList.push(function () {
        console.log(index)
    })
};

functionList.forEach(function (func) {
    func()
});

运行结果:

Debugger listening on ws://127.0.0.1:44616/7a55c820-0524-4493-85ef-9ac413996418
Debugger attached.
1
2
3

上面两种写法的原理很简单,就是 把每一次循环的index作用域控制在当前循环中 。所以很多情况下, ES6真是友好得不得了。建议大家可以学一下ES6。

变量覆盖

var声明变量时候不会去检查有没有重名的变量名,例如:

var origin = 'Hello World';
var origin = 'second Hello World';
console.log(origin);

运行结果:

Debugger listening on ws://127.0.0.1:24251/3a808b2e-c3f9-410c-b216-e4f6cba7046f
Debugger attached.
second Hello World

看似很平常的表现,但是如果在项目工程中覆盖了某个已经在之前声明的变量,那么后果是无法预计的。那 let ( const也一样 ) 声明一个变量有什么好处呢?改一下代码:

let origin = 'Hello World';
let origin = 'second Hello World';
console.log(origin);

const ORIGIN = 'Hello World';
const ORIGIN = 'second Hello World';
console.log(ORIGIN);

然后,运行就报了一个错误:

SyntaxError: Identifier 'origin' has already been declared

SyntaxError: Identifier 'ORIGIN' has already been declared

说明用 let 或者 const 关键字声明变量会预先检查是否有重名变量,如果存在的话会给出错误。神器啊…

作用域链

所谓的 作用域链,笔者的理解就是 访问某个变量所经历的维度形成的链式路径。可能有误或者不专业,望朋友们多多海涵哈哈… 千言万语不敌一段代码,下面直接上代码吧:

var origin = 'Hello World';

function first(origin) {
    second(origin);
}

function second(origin) {
    third(origin);
}

function third(origin) {
    console.log(origin)
}

first(origin);

运行后会如预期一样打印:

Debugger listening on ws://127.0.0.1:29015/4092f9c8-d65d-4b91-ab95-e3ba99ef1860
Debugger attached.
Hello World

因为读取某个变量会首先检查该函数体中有没有 origin,如果没有的话会一直循着调用栈一直往上找,如果到 window 还没找到的话会抛出:

ReferenceError: origin is not defined

如果仍有疑问可直接看图:

《用简单的方式解释传说中的作用域与作用域链》

但如果我们在 second方法 中再定义一个 origin变量会怎么样?

var origin = 'Hello World';

function first(origin) {
    second(origin);
}

function second(origin) {
    var origin = 'second Hello World';
    third(origin);
}

function third(origin) {
    console.log(origin)
}

first(origin);

看运行结果:

Debugger listening on ws://127.0.0.1:15222/ee92e38f-833e-4983-8765-9514495c2bc5
Debugger attached.
second Hello World

此时打印的字符是在second中定义的字符,所以我们可以猜到 读取变量只要读取到对应的变量名就会停止查找,不会继续向上找

《用简单的方式解释传说中的作用域与作用域链》

简单得介绍完作用域链后,本篇博客也结束了。也是笔者目前写得最长的一篇。为了犒劳自己,今晚吃什么呢?哈哈…

    原文作者:风吹过的夏夜
    原文地址: https://segmentfault.com/a/1190000015723305
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞