JavaScript 中变量、函数和 `this` 的作用域和提升
关于这个主题的博客文章和文章很多(以下是参考列表),所以我将尝试重点介绍初级到中级 Web 开发学生学习 JavaScript 时常见的陷阱。
虽然 JavaScript 是一种解释型语言,但在 Web 开发环境中实现它需要执行之前立即执行一个称为词法作用域(标记化)的步骤,在这个步骤中,解释器会浏览你的代码,识别你声明的所有变量,记录它们何时被重新赋值,并将代码块划分为三个级别的作用域:块、函数和全局。
// Example 1 (credit MDN)
function exampleFunction() {
var x = "declared inside function"; // x can only be used in exampleFunction
console.log("Inside function");
console.log(x);
}
console.log(x); // Causes Reference Error
函数的作用域包含变量 `x` x,因此该变量仅在该函数内部有效。尝试在全局作用域中访问它会抛出错误,因为 `x`x不是已声明的变量(甚至不是 `yield` undefined)。
如果我们把这个var声明移到函数外面,它就位于全局作用域内,所有人都知道它的存在,我们可以在函数内部和外部访问它:
// Example 2 (credit MDN)
var x = "declared outside function";
exampleFunction();
function exampleFunction() {
console.log("Inside function");
console.log(x);
}
console.log("Outside function");
console.log(x);
随着 ECMAScript 2015(又称“ES6”)的发布,引入了两种新的变量声明方式:`__init__`let和`__init__`。const这两种方式意义重大,因为它们能够更精细地控制变量的作用域。`__init__`let和const`__init__` 都定义了局部变量,这些变量仅在其定义层级(代码块、函数及其包含的子代码块)内可用。
吊装
在以下示例中,x`a` 是用 `__init__` 声明的var,并且名为 `a` 的同一个变量x在整个函数中(甚至在它前面的行中!)以及子代码块中都可用。如果`a`x是用更新的 `__init__`let或`__init__` 声明的const,那么外部作用域就无法访问它,如果我们let x;在子代码块中再次使用 `__init__`,它实际上就变成了一个不同的变量(就像出生时就分开但被赋予相同名字的人类双胞胎不是同一个人一样)。
// Example 3 (credit MDN)
function varTest() {
var x = 1;
if (true) {
var x = 2; // same variable!
console.log(x); // 2
}
console.log(x); // 2
}
function letTest() {
let x = 1;
if (true) {
let x = 2; // different variable
console.log(x); // 2
}
console.log(x); // 1
}
这一点很重要,因为词法分析步骤紧随代码执行之前,而且:
var x = 5;
这段代码包含两个步骤,列在一行中:声明变量x,并将整数 5赋值给该变量。我们也可以这样写:
var x; // declaration
x = 5; // assignment
当变量用 `&` 声明时var,它的值为 `n` undefined,但我们知道它是一个变量。然后下一行代码将`n` 的值设为 5。但是,对于 `&` 和`&`来说x,情况并非如此。欢迎来到时间死区。letconst
// Example 4 (credit MDN)
function doSomething() {
console.log(bar); // undefined
console.log(foo); // ReferenceError
var bar = 1;
let foo = 2;
}
在这个函数内部,声明bar会被提升到作用域的顶部,在本例中,就是函数内部的代码顶部doSomething()。所以,实际上,它的执行方式如下:
// Example 5
function doSomething() {
var bar;
console.log(bar); // undefined
console.log(foo); // ReferenceError
bar = 1;
let foo = 2;
}
这就是为什么尝试这样做会得到console.log(bar)结果undefined(我们知道它是一个变量,只是没有值),而console.log(foo)抛出引用错误(“一个名为‘foo’的变量?你在说什么,人类?”)
这使得诸如此类的事情成为可能:
// Example 6 (credit MDN)
num = 6;
console.log(num); // returns 6
var num;
和:
// Example 7 (credit MDN)
dogName("Watson");
function dogName(name) {
console.log("My dog's name is " + name);
}
// "My dog's name is Watson"
在第一个例子中,虽然看起来变量var num是在赋值之后才声明的,但从计算机的角度来看,它已经注意到我们在相关的作用域(全局)中声明了它,并将该声明提升到作用域顶部,然后继续执行剩余的代码。在第二个例子中,即使我们在定义函数之前就调用了它,该定义也会被提升到作用域顶部,因此当我们实际开始执行代码时,解释器已经知道该变量是什么dogName()了。
对于var变量,请注意只有声明会被提升,赋值不会被提升。所以,在示例 6 中,像这样写:
console.log(num); // returns undefined
var num = 6;
返回undefined。因此,通常建议始终在变量所在作用域的顶部声明变量,以便记住解释器执行代码的顺序。或者,使用 `this`let和 `this`const可以提供一些保护,防止出现这种行为,因为以这种方式声明的变量不会被初始化为 `null` 值undefined。所以即使它们被提升,你仍然会收到引用错误,因为它们在被赋值之前不会被初始化。这几乎就像它们根本没有被提升一样。`this`const还有一个额外的优点,那就是可以防止意外的重新赋值(尽管以这种方式声明的对象仍然可能会被修改其属性),如下所示:
// Example 8 (credit Digital Ocean)
// Create a CAR object with two properties
const CAR = {
color: "blue",
price: 15000
}
// Modify a property of CAR
CAR.price = 20000;
console.log(CAR);
// Output
// { color: 'blue', price: 20000 }
同样,函数也遵循类似的规则。函数声明会被提升:
// Example 9 (credit Elizabeth Mabishi at Scotch.io)
hoisted(); // Output: "This function has been hoisted."
function hoisted() {
console.log('This function has been hoisted.');
};
……而函数表达式则不是:
// Example 10 (credit Elizabeth Mabishi at Scotch.io)
expression(); //Output: "TypeError: expression is not a function
var expression = function() {
console.log('Will this work?');
};
它如何this融入其中
还有一个相关的话题,我会在讨论作用域和提升时提及this。Gordon Zhu 制作了一份很棒的速查表,总结了他在其Watch & Code课程中关于此主题的精彩内容。本质上,它取决于作用域(上下文)。如果在普通函数内部或外部调用,则指向 window 对象。如果在作为方法调用的函数中,则指向被操作的对象(即点左侧的对象)。thisthisthis
// Example 11 (credit Gordon Zhu)
var myObject = {
myMethod: function() {
console.log(this);
}
};
myObject.myMethod(); // --\> myObject
这种用法的扩展是,当它在构造函数中使用时,如示例 12 所示,其中它this指的是 Person 类的实例:
// Example 12 (credit MDN)
function Person(name, age) {
this.name = name;
this.age = age;
}
this显式地设置call、bind 和 apply的值超出了本博客文章的范围(哈哈!)。
this在回调函数中,这取决于调用位置(作用域) ,这与前面的例子一致。
参考
- JavaScript 的词法作用域、提升和闭包机制不再神秘 ― @nickbalestra
- 理解 JavaScript 中的变量、作用域和提升机制 ― DigitalOcean
- 理解 JavaScript 中的提升机制 ― Elizabeth Mabishi,Scotch.io
- 提升 — MDN Web 文档词汇表:Web 相关术语的定义
- 范围 — MDN Web 文档词汇表:Web 相关术语的定义
- 语法和类型 — JavaScript MDN
原文发表于 donnacodes.com, 日期为 2018 年 8 月 18 日
文章来源:https://dev.to/ddhogan/scope-and-hoisting-of-variables-functions-and-this-in-javascript-5176