第一章:JavaScript变量-灵析社区

懒人学前端

一、var、let、const 的差异?

相同点

varletconst 三者都可以声明变量。变量可以看作盒子,变量名就是盒子名称,值是放在盒子里的东西。

// var 声明变量,初始值可选
var name;
var name = "Lucy";

// let 声明变量,初始值可选
let age;
let age = 12;

// const 声明常量,必须要赋初始值
const city = "Beijing";
// 如果给常量重新赋值会报错
const city = "Shanghai"; // Uncaught SyntaxError: Identifier 'city' has already been declared

// 如果常量的值是对象(数组),不可以修改常量指向的引用,但是可以修改引用的值
const cities = ["Beijing"];
cities[0] = ["Shanghai"]; // "Shanghai"

差异

varletconst 三者的差异:

注:

暂时性死区:Temporal dead zone(TDZ)

全局属性:是否会被添加到 windowglobalThis 等对象中

暂时性死区

从一个代码块的开始直到代码执行到声明变量的行之前,letconst 声明的变量都处于“暂时性死区中。简单理解:let const 只能先声明再访问。如下面的代码:

// 访问 person 变量
// 变量 person 使用 let 声明,声明不会提升,因此此处访问会报错
console.log(person);  // ReferenceError: person is not defined

// 声明 person 
let person = {
    name: "Lucy"
};

同理,使用 const 声明变量,如果在声明前使用,表现与 let 一致。var 声明的全局变量会进行变量提升:

console.log(person); // undefined

var person = {
    name: "Lucy"
}; 

全局属性

var 声明的变量会被添加到全局对象中,可以使用 window globalThis 访问,let const 声明的全局变量则不会添加到全局对象中。

var name = "Lucy";
console.log(window.name); // "Lucy"
console.log(globalThis.name); // "Lucy"

const age = 12;
console.log(window.age); // undefined
console.log(globalThis.age); // undefined

let gender = "female";
console.log(window.gender); // undefined
console.log(globalThis.gender); // undefined

小练习

以下代码输出结果是什么?

const obj = { prop: 0 };
obj.prop = obj.prop + 1;

console.log(obj.prop); // 1. 打印结果是什么?
obj = {}; // 2. 执行结果是什么?

答案:1 处输出 1, 2 处报错: “Uncaught TypeError: Assignment to constant variable”。这与 const 的特性有关,const 仅仅意味着变量名和值的绑定是不可变的,但是它的值可以是可变的,如 1 处 的值 { prop: 0 } 就可以更改。因此,我们可以得出结论:1 处 obj 的值是对象,可以改变 obj 的属性,然而不能重新给 obj 赋新的值(2 处)。

二、谈谈作用域?

作用域

作用域是当前的执行上下文,值和表达式在其中“可见”或可被访问。

静态作用域和动态作用域

作用域分为静态作用域(又称为词法作用域)和动态作用域:

  • 静态意味着它与代码的位置有关,与执行代码时的环境无关。JavaScript 采用的是静态作用域。
  • 动态即运行时,代码执行时确定的。

JavaScript 作用域

JavaScript 的作用域可以分为以下四种:

  • 全局作用域:脚本模式运行所有代码的默认作用域
  • 函数作用域:由函数创建的作用域
  • 块级作用域:用一对花括号(一个代码块)创建出来的作用域
  • 模块作用域:模块模式中运行代码的作用域

接下来,我们分析一下各种作用域的特点。

全局作用域

JavaScript 变量作用域是嵌套的,它们形成树:

  • 最外面的作用域是树的根部,也叫全局作用域。
  • 被最外层作用域包含的作用域是根的子孙,如各种嵌套的代码块形成的作用域。

全局作用域中的变量称为全局变量,可以在任何作用域内访问。有两种全局变量:

  • 全局声明变量(declarative variables)是普通变量,在最顶级由 constlet class 声明的变量。
  • 全局对象(object variables)是存储在全局对象中的属性:

在最顶级由 var function 声明后创建的变量

全局对象可以通过 globalThis window 访问,它可以对全局对象变量进行增删改查

全局属性 globalThis 包含全局的 this 值,类似于全局对象。globalThis 提供了一个标准的方式来获取不同环境下的全局 this 对象(也就是全局对象自身),它可以在任何环境下使用。

下面的代码可以帮助我们更好理解 globalThis 和两种全局变量:

<script>
const declarativeVariable = 'd';
var objectVariable = 'o';
</script>

<script>
// 所有脚本共享同样的顶级作用域
console.log(declarativeVariable); // 'd'
console.log(objectVariable); // 'o'

// 不是所有变量都会创建为全局对象的属性
// 此处最顶层 const 创建的是全局声明变量,不是全局对象,因此 window 对象下访问为 undefined
console.log(window.declarativeVariable); // undefined
console.log(window.objectVariable); // 'o'
// globalThis 同 window 表现一致
console.log(globalThis.declarativeVariable); // undefined  
console.log(globalThis.objectVariable); // 'o'
</script>

观察以上的代码,就能明白为什么最顶层 const 创建的变量,使用 window 访问依然是 undefined。

函数作用域

函数内声明的变量,只能在函数作用域范围内访问。

function scope() {
	var address = "Beijing";
}
console.log(address); // Uncaught ReferenceError: address is not defined 

块级作用域

作用域对变量来说,可以简单理解为程序能够访问到变量的范围,超过作用域的就无法访问。

let const

letconst 支持块级作用域。如果在代码块 {...} 中使用 let const 声明一个变量,那么这个变量只在该代码块中可见。

{ 
    // 作用域 A,可以访问变量 x
    const x = 0; 
    console.log(x); // 0
    {
        // 作用域 B,可以访问 x、y
        const y  = 1;
        console.log(x); // 0
        console.log(y); // 1
    }
}
// 作用域 A 外,不能访问 x、y
console.log(x); // 报错:Uncaught ReferenceError: x is not defined

我们分析一下上面的代码:

  • 作用域 A 是变量 x 的作用域
  • 作用域 B 是作用域 A 的内部作用域
  • 作用域 A 是作用域 B 外部作用域

每个变量可访问的范围是它所在的作用域以及该作用域所嵌套的外部作用域。letconst 声明的变量是块级的,因此它们作用域始终在块中。同一作用域内,不允许声明同名变量。如下示例代码用 const 声明同名变量:

{
    const x = 1;
    const x = 2; // 报错:Uncaught SyntaxError: Identifier 'x' has already been declared
}

不同作用域下可以使用同名变量:

{
    const x = 1;
    {
        const x = 2; 
    }
}

class

class 声明的类也支持块级作用域。如下面代码,在全局作用域中声明了 Animal

class Animal {}
console.log(Animal); // class Animal {}

如果在代码块 {...} 中创建 Animal,此时在代码块外部就无法访问 Animal了。

{
	class Animal {}
	console.log(Animal) // class Animal {}
}

console.log(Animal); // Uncaught ReferenceError: Animal is not defined 

模块作用域

每个 ECMAScript 模块(ES6 Modules)都有自己的作用域,因此,在顶级模块中声明的变量不是全局的。如下图所示:

总结

作用域是值或者表达式的可访问范围。分为静态作用域(词法作用域)和动态作用域,JavaScript 采用的是静态作用域。其中,分为四种不同的作用域:全局作用域、函数作用域、块级作用域以及模块作用域。

除此之外,文中还提到了各种声明与作用域的关系,下面总结一下各种声明的异同。我们从以下四个方面来看各种声明的异同:

  • 作用域
  • 暂时性死区:变量何时可以访问?一些变量在进入作用域后可尽快被访问,但有的必须等到代码执行到它们声明时才可以访问。通俗说法是,这个变量是否可以先访问再声明。暂时性死区(TDZ, Temporal Dead Zone) 就是变量在进入作用域和执行声明前的一段时间。在这段时间内,访问变量会报错。
  • 重复:变量是否可以重复声明(同级作用域下)
  • 全局属性:声明的变量是否会被添加到全局对象中

三、什么是变量提升?

变量提升和上文提到触发时间有关。我们知道,varfunction 声明的变量可以在声明前访问,这就是因为变量提升的缘故。

当 JavaScript 引擎执行代码时,创建了全局执行上下文,它有两个阶段:

  • 创建(准备工作)
  • 执行

在创建阶段,JavaScript 引擎将 var function 声明移到了顶层,这就是 JavaScript 的变量提升。

var 关键字

我们先看一段代码:

console.log(counter); // undefined
var counter = 1;

在这段代码中,我们在声明前访问 counter 变量,并未报错。这是变量提升的缘故。

function 提升

var 一样,函数声明也会提升:

let x = 20,
	y = 10;

let result = add(x, y);
console.log(result); // 30

function add(a, b) {
	return a + b;
}

除此之外,let 关键字、函数表达式、箭头函数等均不会变量提升。

处理相同的变量名或者函数名

代码中出现相同的变量或者函数怎么办?我们知道 var 声明的同名变量,后者会覆盖前者,如果是函数呢?先看下面的例子:

let x = 20,
	y = 10;

function add(a, b) {
	return a + b;
}

let result = add(x, y);
console.log("result " + result); // "result 60"

function add(a) {
	return a + 40;
}

let result1 = add(x);
console.log("result1 " + result); // "result1 60"

从上面的结果可以看到,两个同名函数 add,定义在后面的函数覆盖了前面的函数,因此 result result1 的结果都是执行后面的函数后返回的结果。

因此,代码中出现同名的变量名或者函数名,都是后者覆盖前者。

小练习

下面的代码输出的结果是什么?

let x = 20,
	y = 10;

let result = add(x);
console.log("result1 " + result); // "result1 60"

var add = function(a, b) {
	return a + b;
}

function add(a) {
	return a + 40;
}

这里考察的是同名变量和函数的提升,后面的会覆盖前面的,因此执行的是最后一个函数 add

阅读量:2011

点赞量:0

收藏量:0