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

今年第十号台风‘安比’来啦,外面狂风大做暴雨连连,哪敢出门啊因此就安心待在家里写一篇博客总结下本身最近学习心得,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 会发生什么事情,经过改写刚才的代码咱们来作个试验:spa

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();

经过运行看一下结果:3d

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米的大刀,我没有在忽悠在读的朋友,请接着往下看。code

函数做用域

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

首先咱们来看一个例子:ip

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

若是仍有疑问可直接看图:

clipboard.png

但若是咱们在 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中定义的字符,因此咱们能够猜到 读取变量只要读取到对应的变量名就会中止查找,不会继续向上找

clipboard.png

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

相关文章
相关标签/搜索