我所理解的作用域(Javascript)

作用域

前言

几乎所有语言都包含作用域概念,除开一些计算机发展史上早期的语言。
看见作用域是多么重要,但是很多语言相关的书籍都没有将作用域当成最重要的部分来讲解。
也就导致了现在很多半吊子程序员很多。因为他们不了解作用域是多么重要。
因为作用域可以说是一门语言从编写到运行整个过程中最基本的一套规则。当然也可能是写书的作者本身就是个半吊子。不能给清楚的洞察语言本身的运行机制。
了解一门语言之前,除开要知道这门语言本身有什么特点,更重要的是要了解其底层执行的机制。
了解作用域是多么的重要,请不要忽视它。
现在很多程序员不会去思考语言本身,而仅仅浮于表面,只会使用(身边不乏这样的人)。
在学习一门语言之前,建议最好搞清楚以下几个问题:

  • 我为什么要学这么语言?(因为大家都在学,这样想法的人,我劝你早点放弃。)
  • 这门语言被设计出来为了解决什么问题?
  • 这么语言与其它语言有什么不同?
  • 这门语言能够在怎样的环境中运行?
  • 这门语言本身有什么特点?
  • 这门语言的规则是什么?
  • 这门语言运行机制是什么?(这个似乎比较难,但是现在网络这么发达,信息量如此之大,难道还找不到?谷歌会吧?不行可以去stackoverflow提问,我相信会有很多热心的网友给你满意的答复的。别TMD给我扯百度。)

如果这几个问题没有搞清楚之前,你学什么都是白学。因为你根本不可能学精。

作用域是什么?

有意思的是,搞了这么多年程序开发。一直没搞清楚我到底在做什么?直到最近几年我才恍然大悟,原来我们一直在搞事情(-v-数据)。
虽然有点晚,但是好在搞清楚了。有点笨(^-^)
写一个程序其实就是用合理的算法去搞一堆数据。(算法与数据结构一定要学好)

  • 如何搞这些数据?
    当然是用各种算法来搞这些数据啦,普通的就是加,减,乘,除。难点无非是排序啦什么的。没有那么神奇的东西。
    在此我们不得不去感谢那些数学先驱们,比如(莱昂哈德·欧拉,约瑟夫·傅里叶, 约翰·卡尔·弗里德里希·高斯,诺姆·乔姆斯基),没有这些人,我们今天还都在玩能吃的苹果。
  • 这些数据在哪里?
    数据保存在硬盘,内存,寄存器,还是其他地方。我们通通把这些地方叫做数据存储的介质。这时候就涉及一个问题了,我们怎样保持这些数据,怎样获取这些数据。
    上个世纪,计算机刚刚起步的时候,我们的程序算法和数据都是通过打孔来运算的。

撸主偏题了。
程序运行的时候会将这些数据中的一部分(需要的)暂时保存在寄存器(CPU)或者内存中,那么它们应该保存在什么位置?什么时候被清除(比较我们的寄存器和内存只有那么大)?
这些都需要建立一套规则来让计算机知道。
作用域就是一套保存数据的规则。

Javascript的作用域

javascript作用域大致分为以下几个部分:

  • 全局作用域
  • 函数内部作用域
  • 类内部作用域(ECMAScript 6)
  • 块内作用域
    基本上所有变量都会安分的呆在自己所在作用域的内,除了个别不安分的(后面会讲到)。
    程序在运行一段时间后会发起一个短暂的内存回收工作(java可能会比较长),这个时候会把某个不再使用的域内的变量都会回收掉。

    全局作用域

    全局作用域,顾名思义就是那个在最顶层,高高在上的家伙,程序的任何角落都能获取和修改的域。
    全局作用域包含所有下层域。下层域可以引用全局域中的数据变量,而全局域却不能去获取下层域的变量。对啊-它就是那么无私。
    如果下层域声明了一个和全局域一模一样的变量,那么对不起你只能通过其他方式来获取全局域的变量了。比如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * 全局域被遮罩
    */
    function DoSomething()
    {
    var beMaskVar = 2;
    console.log(beMaskVar); // 2
    console.log(window.beMaskVar); // 1
    }
    var beMaskVar = 1;
    DoSomething();
    这个就是全局域被遮罩
    为什么会出现这样的效果呢?
    因为javascript引擎是自下而上(自内而外)从作用域查找变量的,如果在内部找到某个变量,那么引擎取到这个变量值之后(RHS)就开始执行下一个动作了,并不用一直向上(外)继续查找。如果要引用上次变量,那么久需要通过其他手段获取,全局变量还可以用window来获取,如果是函数(除开函数表达式)可能真就找不到了。
    不是所有变量都应该声明在全局。因为:
    • GC不知道到底什么时候去回收
    • 违反了最小权限原则

从ECMAscript 5标准发布开始,javascript多了一种严格模式(use strict), 这种模式和非严格模式最大的不同在于,引擎在搜索变量至最顶层(全局域)任然没有找到变量之后的操作方式不一样。运行在非严格模式下引擎会自动给你创建一个变量,而严格模式下会抛出ReferenceError

函数作用域

函数作用域即是在函数内部的作用域。

1
2
3
4
5
6
function ScopeFun(innerVar) {
var funVar = 5;
console.log('innerVar + funVar = ', (innerVar + funVar)); // innerVar + funVar = 100
}
ScopeFun(95);

当这个函数执行完成或者不在被外部其他作用域应用的时候(闭包),函数内部的变量就会被标记为回收等待被回收。
1
2
3
4
5
6
7
8
9
var closure = {};
(function (c) {
var funVar = 5; //这个变量不会被回收
c.Add = function(outVar){
console.log('outVar + funVar = ', (outVar + funVar)); // innerVar + funVar = 100
};
}(closure));
closure.Add(95);

这个就是一个简单的闭包,后里面的会专门针对闭包写一篇文章。

当函数记住并可以引用所在词法作用域时就产生了闭包,并且该函数可以在词法作用域外被执行。

类作用域

类的概念是在2015年6月发布的ECMAScript 6标准之后才被引入的。
首先,我们先来了解一下类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Animal {
constructor(name) {
this.name = name;
}
SayHi(someOne) {
console.log('hi.');
}
}
class Person extends Animal{
SayHi(someOne) {
console.log('hi ' + someOne + ', i am ' + this.name + '.' );
}
}
var ps = new Person('tom');
ps.SayHi('peter'); // hi peter, i am tom.

是不是和其他语言声明一个类类似?
this关键字将类内部变量作用域串连起来,所有内部变量均可通过此引用或赋值。

块作用域

有时候我们需要在某个全局作用域或函数作用域内搞一个内部块状作用域。
比如:

1
2
3
4
5
6
7
function add(j) {
i = 2;
console.log(j, j * i);
}
for (var i=0;i<6;i++) {
add(i);
}

这个代码输出会如下面吗?
1
2
3
4
5
6
7
// 0, 0
// 1, 2
// 2, 4
// 3, 6
// 4, 8
// 5, 10
// 6, 12

答案: 不是
这个会造成一个死循环。
为什么会这样?
因为函数add内部引用了全局变量i,并在函数末尾给他赋值等于了2.
怎样去改善呢? 有人说内部再申明一个变量k = i就好了。
其实我们有更好的办法
1
2
3
4
5
6
for (var i=0;i<6;i++) {
{
let j = i;
console.log(j, j * 2);
}
}

一点小魔法

其实也不是什么魔法,就是一些小方法而已。
不过按照大神的说法,不建议使用这些方法
因为,会导致性能严重下降
但是我们还是会简单介绍一下,就简单的介绍一下。
就两个方法: evalwith

eval函数

它可以接受一个字符串为参数,并将其中的内容视成编写代码的时候就卸载这里一样运行它。
eval函数会动态的将你所传入的字符串在当前词法域中执行,并且当前词法域动态修改。
比如以下代码:

1
2
3
4
5
6
7
function ScopeFun(evalStr, var1) {
eval(evalStr);
console.log(evalVar, var1);
}
ScopeFun('var evalVar=1;', 2); // 1, 2
console.log(evalVar); // ReferenceError

但是在严格模式下,上面的代码输出又发生了改变。

严格模式下,eval执行有自己专属的词法作用域.


测试下面代码:
1
2
3
4
5
6
7
function ScopeFun(evalStr, var1) {
'use strict';
eval(evalStr); // 1
console.log(evalVar, var1); // ReferenceError
}
ScopeFun('var evalVar=1;console.log(evalVar);', 2);

当然还有一些和eval差不多的方法。

setTimeout 和 setInterval两个方法的第一个参数也可以是字符串。字符串内容可以被解释为一段动态生成的函数代码。

with方法

with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。
就像是这样:

1
2
3
4
5
var person = {
name: '',
sex: '',
age: ''
};

如果按照正常的方法,一般是按以下方式来赋值。
1
2
3
person.name = 'tom';
person.sex = 'male';
person.age = 'secret';

这样虽然繁琐,但是很安全
当然也可以这样:
1
2
3
4
5
with(person) {
name = 'tom';
sex = 'male';
age = 'secret';
}

虽然with的确给我们在编写代码上带来了不少便利,但是其带来的隐患远远大于便利。
请看下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var person = {
name: '',
sex: '',
age: ''
};
function setWith() {
with(person) {
name: 'tom';
weight: '61kg';
}
}
console.log(person);
/* person = {
name: 'tom',
sex: '',
age: ''
};
*/

这里输出的数据明显和我们设置的不一样, weight到哪里去了?
1
console.log(weight); // 61kg

with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域, 因此这个对
象的属性也会被处理为定义在这个作用域中的词法标识符。


这个是意想不到的结果。

总结

javascript总是给我们带来很多精彩,但是也要小心其中的黑魔法。
如果善用不当,会造成意想不到的结果。
感谢你耐心看完本文。

– End –

附录

Javascript 严格模式详解
Javascript 类