一、作用域相关点
1、一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量。这套规则被称为作用域。
2、变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
3、LHS 查询:赋值操作的目标是谁;RHS查询:谁是赋值操作的源头。
4、当引擎执行LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。(严格模式禁止自动或隐式地创建全局变量)。
5、eval方法相关
5-1、eval(…) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置的一样。
5-2、如果eval(…) 中所执行的代码包含有一个或多个声明(无论是变量还是函数),就会对eval(…) 所处的词法作用域进行修改。
function foo(str, a) {
eval( str ); // 欺骗!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
/*
示例解析:eval(..) 调用中的"var b = 3;" 这段代码会被当作本来就在那里一样来处理。
由于那段代码声明了一个新的变量b,因此它对已经存在的foo(..) 的词法作用域进行了修改。
事实上,和前面提到的原理一样,这段代码实际上在foo(..) 内部创建了一个变量b,
并遮蔽了外部(全局)作用域中的同名变量。
*/
5-3、在严格模式的程序中,eval(…) 在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。
function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );
6、with关键字相关
6-1、with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。
var obj = {
a: 1,
b: 2,
c: 3
};
// 单调乏味的重复"obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}
6-2、尽管with 块可以将一个对象处理为词法作用域,但是这个块内部正常的var声明并不会被限制在这个块的作用域中,而是被添加到with 所处的函数作用域中。
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!
7、词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。
8、作用域链是基于调用栈的,而不是代码中的作用域嵌套。
function foo() {
console.log( a ); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
/*
代码解析:词法作用域让foo() 中的a 通过RHS 引用到了全局作用域中的a,因此会输出2。
*/
9、eval 和 with 这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。
二、函数作用域相关
1、函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。
2、包装函数的声明以(function… 而不仅是以function… 开始。尽管看上去这并不是一个很显眼的细节,但实际上却是非常重要的区别。函数会被当作函数表达式而不是一个标准的函数声明来处理。
注意点:区分函数声明和表达式最简单的方法是看function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
var a = 2;
(function foo(){ // <-- 添加这一行
var a = 3;
console.log( a ); // 3
})(); // <-- 以及这一行
console.log( a ); // 2
/*
代码解析:
*1、(function foo(){ .. }) 作为函数表达式意味着foo 只能在.. 所代表的位置中被访问,
外部作用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部作用域。
*2、(function foo(){ .. })()。第一个( ) 将函数变成表达式,第二个( ) 执行了这个函数。
*/
3、相较于传统的IIFE 形式,很多人都更喜欢另一个改进的形式:(function(){ … }())。
三、块作用域相关
1、JavaScript 的ES3 规范中规定try/catch 的catch 分句会创建一个块作用域,其中声明的变量仅在catch 内部有效。
2、ES6 中 let 关键字可以将变量绑定到所在的任意作用域中(通常是{ … } 内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。
3、ES6 还引入了const,同样可以用来创建块作用域变量,但其值是固定的(常量)。
四、提升(变量和函数声明)
1、包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
var a = 2;
/*
代码解析:第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。
*/
2、函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个“重复”声明的代码中)是函数会首先被提升,然后才是变量。
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
/*
代码解析:会输出1 而不是2 !,var foo 尽管出现在function foo()... 的声明之前,
但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。
*/
五、闭包
1、当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
2、在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

本文详细探讨了JavaScript中的作用域概念,包括作用域的基本规则、函数作用域、块作用域、提升现象、闭包特性以及eval和with关键字的影响。通过具体示例,解释了变量声明、函数声明和作用域链的运作机制。

被折叠的 条评论
为什么被折叠?



