与大多数其他语言相比,Javascript 具有许多特殊性,这就是为什么许多人喜欢或讨厌它的原因。 其中,变量的作用域是很多初学者的一个“坑”。变量的作用域是编程技巧中的一个基本概念,而在Javascript中,这个基本概念经常挑战初学者的常识。
您可以通过查看JavaScript源代码来判断标识符的范围。这四个范围是:
在全局和模块作用域的特殊情况之外,使用var(函数作用域)、let(块作用域)和const(块作用域)声明变量。大多数其他形式的标识符声明在严格模式下具有块范围。
先上例子:
var scope = 'global';
function checkScope(){
var scope = 'local';
console.log(scope); // local
}
checkScope();
console.log(scope); // global
在上面的例子中,声明了函数体中的全局变量作用域和局部变量作用域。 在函数体内,局部变量的优先级高于透明的全局变量。 如果局部变量的名称与全局变量的名称相同,则局部变量将在声明该局部变量的函数体范围内覆盖同名的全局变量。 .
下面再看一个例子:
scope = 'global';
function checkScope(){
scope = 'local';
console.log(scope); // local
myScope = 'local';
console.log(myScope); // local
}
checkScope();
console.log(scope); // local
console.log(myScope); // local
对于初学者来说,可能有两个问题:为什么在函数外把scope的值改成了local? 为什么可以在函数外访问 myScope 变量?
这两个问题都源于一个特征。 在全局作用域中声明变量可以省略var关键字,但是如果在函数体中声明变量时没有使用var关键字,就会出现上述现象。 首先,函数体中的第一行语句改变了全局变量中作用域变量的值。 在声明 myScope 变量时,由于没有使用 var 关键字,Javascript 将在全局范围内声明变量。 因此,在声明局部变量时使用 var 关键字是一个好习惯。
在 C 或者 Java 等语言中,if、for 等语句块内可以包含自己的局部变量,这些变量的作用域是这些语句的语句块,而在 Javascript 中,不存在「块级作用域」的说法。
看下面的例子:
function checkScope(obj){
var i = 0;
if (typeof obj == 'object') {
var j = 0;
for (var k = 0; k < 10; k++) {
console.log(k);
}
console.log(k);
}
console.log(j);
}
checkScope(new Object());
在上面的例子中,每一条控制台输出语句都能输出正确的值,这是因为,由于 Javascript 中不存在块级作用域,因此,函数中声明的所有变量,无论是在哪里声明的,在整个函数中它们都是有定义的。
如果要更加强调上文中 函数中声明的所有变量,无论是在哪里声明的,在整个函数中它们都是有定义的 这句话,那么还可以在后面跟一句话:函数中声明的所有变量,无论是在哪里声明的,在整个函数中它们都是有定义的,即使是在声明之前。对于这句话,有个经典的困扰初学者的「坑」。
var a = 2;
function test(){
console.log(a);
var a = 10;
}
test();
上面的例子中,控制台输出变量 a 的值为 undefined,既不是全局变量 a 的值 2,也不是局部变量 a 的值 10。首先,局部变量在整个函数体内都是有定义的,因此,局部变量 a 会在函数体内覆盖全局变量 a,而在函数体内,在 var 语句之前,它是不会被初始化的。如果要读取一个未被初始化的变量,将会得到一个默认值 undefined。
所以,上面示例中的代码与下面的代码时等价的:
var a = 2;
function test(){
var a;
console.log(a);
a = 10;
}
test();
可见,把所有的函数声明集合起来放在函数的开头是个良好的习惯。
可能很多人已经注意到,在 Javascript 当中,一个变量与一个对象的一个属性,有很多相似的地方,实际上,它们并没有什么本质区别。在 Javascript 中,任何变量都是某个特定对象的属性。
全局变量是全局对象的属性。 在 Javascript 解释器开始运行且未执行 Javascript 代码之前,会创建一个“全局对象”,然后 Javascript 解释器会给它并定义一些属性。 这些属性是我们可以直接在 Javascript 代码中使用的内置变量。 和方法。 之后,每当我们定义一个全局变量时,实际上就是为全局对象定义了一个属性。
在客户端Javascript中,这个全局变量就是Window对象,它有一个属性window指向自己,也就是我们常用的全局变量。
对于函数的局部变量,当函数开始执行时,会创建一个对应的“调用对象”,并将函数的局部变量作为其属性存储。 这可以防止局部变量覆盖全局变量。
如果要深入理解 Javascript 中变量的作用域,那就必须拿出「作用域链」这个终极武器。
首先要理解的一个名词就是「执行环境」,每当 Javascript 执行时,都会有一个对应的执行环境被创建,这个执行环境中很重要的一部分就是函数的调用对象(前面说过,调用对象是用来存储相应函数的局部变量的对象),每一个 Javascript 方法都是在自己独有的执行环境中运行的。简而言之,函数的执行环境包含了调用对象,调用对象的属性就是函数的局部变量,每个函数就是在这样的执行环境中执行,而在函数之外的代码,也在一个执行环境中,这个执行环境包含了全局变量。
在 Javascript 的执行环境中,还有一个与之对应的「作用域链」,它是一个由对象组成的列表或链。
当 Javascript 代码需要查询一个变量 x 的时候,会有一个被称为「变量名解析」的过程。它会首先检查作用域链的第一个对象,如果这个对象包含名为 x 的属性,那么就采用这个属性的值,否则,会继续检查第二个对象,依此类推。当检查到最后一个对象的时候仍然没有相应的属性,则这个变量会被认定为是「未定义」的。
在全局的 Javascript 执行环境中,作用域链中只包含一个对象,就是全局对象。而在函数的执行环境中,则同时包含函数的调用对象。由于 Javascript 的函数是可以嵌套的,因此每个函数执行环境的作用域链可能包含不同数目个对象,一个非嵌套的函数的执行环境中,作用域链包含了这个函数的调用对象和全局对象,而在嵌套的函数的执行环境中,作用域链包含了嵌套的每一层函数的调用对象以及全局变量。
我们可以用一个图来直观地解释作用域链和变量名解析的过程:
以下将抛出一个 ReferenceError 因为名称x, y, 和z在函数之外没有意义f。
function f() {
var x = 1
let y = 1
const z = 1
}
console.log(typeof x) // undefined (because var has function scope!)
console.log(typeof y) // undefined (because the body of the function is a block)
console.log(typeof z) // undefined (because the body of the function is a block)
以下内容将为yand抛出 ReferenceError z,但不会为 for抛出一个 ReferenceError x,因为 的可见性x不受块的约束。定义控制结构的体块一样if,for和while,行为类似。
{
var x = 1
let y = 1
const z = 1
}
console.log(x) // 1
console.log(typeof y) // undefined because `y` has block scope
console.log(typeof z) // undefined because `z` has block scope
在下面,x在循环外可见,因为var具有函数作用域:
for(var x = 0; x < 5; ++x) {}
console.log(x) // 5 (note this is outside the loop!)
由于这种行为,您需要小心关闭使用varin 循环声明的变量。这里只x声明了一个变量实例,它在逻辑上位于循环之外。
以下打印5,五次,然后在循环外打印5第六次console.log:
for(var x = 0; x < 5; ++x) {
setTimeout(() => console.log(x)) // closes over the `x` which is logically positioned at the top of the enclosing scope, above the loop
}
console.log(x) // note: visible outside the loop
以下打印undefined因为x是块范围的。回调是一一异步运行的。新行为let变量意味着每个匿名函数关闭了一个名为不同的变量x(不像它会用做var),所以整数0通过4印:
for(let x = 0; x < 5; ++x) {
setTimeout(() => console.log(x)) // `let` declarations are re-declared on a per-iteration basis, so the closures capture different variables
}
console.log(typeof x) // undefined
以下不会抛出 aReferenceError因为 的可见性x不受块的约束;但是,它会打印,undefined因为变量尚未初始化(因为if语句)。
if(false) {
var x = 1
}
console.log(x) // here, `x` has been declared, but not initialised
在for循环顶部声明的变量usinglet的作用域是循环体:
for(let x = 0; x < 10; ++x) {}
console.log(typeof x) // undefined, because `x` is block-scoped
以下将抛出 aReferenceError因为 的可见性x受块约束:
if(false) {
let x = 1
}
console.log(typeof x) // undefined, because `x` is block-scoped
使用var, letor声明的变量const都作用域为模块:
// module1.js
var x = 0
export function f() {}
//module2.js
import f from 'module1.js'
console.log(x) // throws ReferenceError
以下将在全局对象上声明一个属性,因为var在全局上下文中使用声明的变量将作为属性添加到全局对象:
var x = 1
console.log(window.hasOwnProperty('x')) // true
let并且const在全局上下文中不向全局对象添加属性,但仍然具有全局作用域:
let x = 1
console.log(window.hasOwnProperty('x')) // false
可以认为函数参数是在函数体中声明的:
function f(x) {}
console.log(typeof x) // undefined, because `x` is scoped to the function
catch 块参数的作用域是 catch 块体:
try {} catch(e) {}
console.log(typeof e) // undefined, because `e` is scoped to the catch block
命名函数表达式的范围仅限于表达式本身:
(function foo() { console.log(foo) })()
console.log(typeof foo) // undefined, because `foo` is scoped to its own expression
在非严格模式下,全局对象上隐式定义的属性是全局范围的。在严格模式下,您会收到错误消息。
x = 1 // implicitly defined property on the global object (no "var"!)
console.log(x) // 1
console.log(window.hasOwnProperty('x')) // true
在非严格模式下,函数声明具有函数作用域。在严格模式下,它们具有块作用域。
'use strict'
{
function foo() {}
}
console.log(typeof foo) // undefined, because `foo` is block-scoped
在ES5中,只全局作用域和函数作用域。这会导致函数作用域覆盖了全局作用域;亦或者循环中的变量泄露为全局变量。用let命令新增了块级作用域,外层作用域无法获取到内层作用域,非常安全明了。
在JavaScript中使用var定义一个变量,无论是定义在全局作用域函数函数的局部作用域中,都会被提升到其作用域的顶部,这也是JavaScript定义变量的一个令人困惑的地方。由于es5没有像其它类C语言一样的块级作用域,因此es6增加了let定义变量,用来创建块级作用域。
作用域是在函数声明的时候就确定的一套变量访问规则,而执行上下文是函数执行时才产生的一系列变量的集合体。也就是说作用域定义了执行上下文中的变量的访问规则,执行上下文是在这个作用域规则的前提下执行代码的。
先声明一个name变量,然后声明一个person对象,person包含name和sayName属性。当直接在对象上进行方法的调用时:person.sayName(),函数的作用域遵循“谁调用就是谁”的原则,sayName的作用域(也就是this)指向的就是person。
有一定JavaScript开发经验的人应该会熟悉下面这种立即执行函数的写法:不过即使不熟悉也没关系,这里我会讲解这种写法的含义。先来看下面这个更容易理解的示例:
几年前,消失的作用域 CSS,如今它回来了,而且比以前的版本要好得多。更好的是,W3C规范基本稳定,现在Chrome中已经有一个工作原型。我们只需要社区稍微关注一下,引诱其他浏览器构建它们的实现
静态作用域指的是一段代码,在它执行之前就已经确定了它的作用域,简单来说就是在执行之前就确定了它可以应用哪些地方的作用域(变量)。 动态作用域–函数的作用域是在函数调用的时候才决定的
作用域是你的代码在运行时,某些特定部分中的变量,函数和对象的可访问性。换句话说,作用域决定了变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。
函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[scope]],由ECMA-262标准第三版定义,该内部属性包含了函数被创建的作用域中对象的集合
ES6新增了两个重要的关键字let和const,相信大家都不陌生,但是包括我在内,在系统学习ES6之前也只使用到了【不存在变量提升】这个特性。let声明一个块级作用域的本地变量
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!