变量
变量是存储数据值的容器。
# 变量声明
ES5 有两种声明变量的方法: var
、function
var sum = 0;
function add(a) {
var sum = a + 1;
return sum
}
2
3
4
5
6
ES6 有六种声明变量的方法:var
、function
、let
、const
、import
、class
// ... var、function 声明和 ES5 一致
import StudentList from './student.js'; // 不需与 student.js 中export 的名称相同,可指定名称
let sum = 0;
const name = '咩';
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
student.js
let student = [
{
name: 'tony',
age: 21,
},
{
name: 'lucy',
age: 18
}
]
export default student;
2
3
4
5
6
7
8
9
10
11
# 作用域
# 执行环境
执行环境(execution context)是 JavaScript 中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个 与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。
每个执行环境都有一个执行环境对象。(this对象有关介绍,请看函数章节)
全局执行环境是最外围的一个执行环境。在 Web 浏览器中,全局执行环境被认为是 window 对象。全局执行环境直到应用程序退 出,例如关闭网页或浏览器时才会被销毁。
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。 而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript 程序中的执行流 正是由这个方便的机制控制着。
# 作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。
- 作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。
- 作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对 象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。
- 作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延 续到全局执行环境。
- 全局执行环境的变量对象始终都是作用域链中的最后一个对象。
# 作用域
- 全局作用域
- 局部作用域
- 块级作用域(es6)
全局变量拥有全局作用域。变量和函数会挂载到 window 对象上。
var scope = 'global'; // 声明一个全局变量
function checkScope () {
var scope = 'local'; // 声明一个同名局部变量
myscope = 'local';
return scope; // 返回局部变量的值, 而不是全局变量的值
}
window.myscope // undefined => checkScope() 还未执行,该变量未声明
checkScope(); // local
window.myscope // local
2
3
4
5
6
7
8
9
10
局部变量是局部作用域,仅在函数体内有用。
var scope = 'global';
function checkScope () {
var scope = 'local';
function nested() {
var scope = 'nested';
return scope;
}
return nested();
}
checkScope() // nested =>返回的是 nested() 的 scope
window.scope // global => 全局变量 scope 并未被覆盖
2
3
4
5
6
7
8
9
10
11
12
函数体内局部变量优先级高于同名全局变量。同名全局变量会被覆盖。
scope = 'global'; // 声明一个全局变量,可以不用 var 声明
function checkScope2 () {
scope = 'local'; // 修改了全局变量 scope
myscope = 'local'; // 显式声明了一个新的全局变量
return [scope, myscope];
}
checkScope2(); // [local, local]
window.scope; // local => 全局变量修改了
window.myscope; // local =>全局命名空间搞乱了
2
3
4
5
6
7
8
9
10
var 声明变量会提升,内部变量可能会覆盖外层变量
var tmp = '哈哈';
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined => 理想情况应该输出值 “ 哈哈 ”
2
3
4
5
6
7
8
9
原因在于,预编译后,if 语句内的 temp 声明提升了
var tmp = '哈哈';
function f() {
var tmp
console.log(tmp); // 打印的是 if 里面提升 temp
if (false) {
tmp = 'hello world';
}
}
f();
2
3
4
5
6
7
8
9
10
变量提升有关请看下面章节介绍
ES5 没有块级作用域。使用不当会造成变量泄露。
for (var k = 0; k < 5; k++) {
setTimeout(function () {
console.log('inside', k);
}, 1000);
}
console.log('outside', k); // outside 5 => 理想情况下,k 仅在 for 循环中有效,这里不应该输出 5,应该提示 k is not defined
// 间隔1s,分别输出5个 inside 5 => 理想情况下,应该输出 0 1 2 3 4
window.k; // 5 => 可看出 k 是全局变量,所以当执行 for 里面的语句时,k已经循环完了5次,此时 k = 5
2
3
4
5
6
7
8
9
10
再来一题
var test = function() {
var arr = [];
for (var i = 0; i < 3; i++) {
console.log('开始循环了', i)
arr[i] = function() {
return i * i;
};
}
return arr;
};
var a = test(); // 输出 “开始循环了 0 1 2” => 此时 arr[i]是还未执行的,i 已经等于 3 了
a[1](); // 9
a[2](); // 9
2
3
4
5
6
7
8
9
10
11
12
13
14
# 块级作用域
通过前面的介绍,可以知道,ES5 是没有块级作用域的。变量使用不当,容易造成变量泄露,出现很多不合理场景。为了避免这种情况出现,我们可以使用以下方法:
以下都是针对ES5 没有块级作用域。使用不当会造成不合理场景
列举的例子进行修改
1. 借助立即执行函数
for (var k = 0; k < 5; k++) {
(function(k){
//这里是块级作用域
setTimeout(function (){
console.log('inside', k);
},1000);
})(k);
}
console.log('outside', k);
// 输出 outside 5
// 再依次输出 inside 0 1 2 3 4
2
3
4
5
6
7
8
9
10
11
12
13
2. 定义函数并传值
var _loop = function _loop(k) {
//这里是块级作用域
setTimeout(function () {
console.log(k);
}, 1000);
};
for (var k = 0; k < 5; k++) {
_loop(k);
}
// 依次输出 0 1 2 3 4
2
3
4
5
6
7
8
9
10
11
12
1、2 写法都是利用了 JS 中调用函数传递参数都是值传递的特点
3. 使用setTimeout的第三个参数
for (let k = 0; k < 5; k++) {
setTimeout(function () {
console.log(k);
}, 1000, k);
}
// 依次输出 0 1 2 3 4
2
3
4
5
6
4. 使用 let、const 声明变量
for (let k = 0; k < 5; k++) {
setTimeout(function () {
console.log(k);
}, 1000);
}
console.log(k); // k is not defined
// 间隔1s,分别输出inside 0 1 2 3 4
2
3
4
5
6
7
8
关注执行顺序
# 变量提升与函数提升
js 执行过程:
- 词法分析阶段:词法分析主要包括:分析形参、分析变量声明、分析函数声明三个部分。通过词法分析将我们写的js代码转成可以执行的代码,接下来才是执行。
- 执行阶段。
一般来说变量和函数会在使用之前就会声明,但是也可以在使用之后再进行声明,这是因为函数声明和变量声明总是会被解释器悄悄地被"提升"到方法体的最顶部。
# 变量提升(Hoisting)
- 只有声明被提升,初始化不会被提升
- 声明会被提升到当前作用域的顶端
例子1:
console.log(num); // undefined
var num;
num = 6;
2
3
预编译后
var num;
console.log(num); // undefined
num = 6; // 初始化不会被提升
2
3
例子2:
num = 6;
console.log(num); // 6 =》 提升了 var sum
var num;
2
3
预编译后
var num;
num = 6;
console.log(num); // 6
2
3
例子3:
console.log(num); // undefined
var num = 6;
2
预编译后
var num;
console.log(num);
num = 6;
2
3
例子4:
function hoistVariable() {
if (!foo) {
var foo = 5;
}
console.log(foo); // 5
}
hoistVariable();
2
3
4
5
6
7
8
预编译后
function hoistVariable() {
var foo // 将if语句内的声明提升
if (!foo) { // !undefined = true
foo = 5;
}
console.log(foo); // 5
}
hoistVariable();
2
3
4
5
6
7
8
9
例子5:
var foo = 3;
function hoistVariable() {
var foo = foo || 5;
console.log(foo); // 5
}
hoistVariable();
2
3
4
5
6
预编译后
var foo = 3;
function hoistVariable() {
var foo
foo = foo || 5; // 此时 等号右侧 foo 为 undefined
console.log(foo); // 5
}
hoistVariable();
2
3
4
5
6
7
# 函数提升
- 函数声明和初始化都会被提升
- 函数表达式不会被提升
例子1:函数声明可被提升
console.log(square(5)); // 25
function square(n) {
return n * n;
}
2
3
4
预编译后
function square = (n) {
return n * n;
}
console.log(square(5)); // 25
2
3
4
例子2:函数表达式不可被提升
console.log(square); // undefined
console.log(square(5)); // square is not a function =》 初始化并未提升,此时 square 值为 undefined
var square = function (n) {
return n * n;
}
2
3
4
5
预编译后
var square
console.log(square); // undefined =》赋值没有被提升
console.log(square(5)); // square is not a function =》 square 值为 undefined 故报错
square = function (n) {
return n * n;
}
2
3
4
5
6
例子3:
function hoistFunction() {
foo(); // 2
var foo = function() {
console.log(1);
};
foo(); // 1
function foo() {
console.log(2);
}
foo(); // 1
}
hoistFunction();
2
3
4
5
6
7
8
9
10
11
12
13
预编译后
function hoistFunction() {
var foo
foo = function foo() {
console.log(2);
}
foo(); // 2
foo = function() {
console.log(1);
};
foo(); // 1
foo(); // 1
}
hoistFunction();
2
3
4
5
6
7
8
9
10
11
12
13
14
# 优先级
- 函数提升在变量提升之前
- 变量的问题,莫过于声明和赋值两个步骤,而这两个步骤是分开的。
- 函数声明被提升时,声明和赋值两个步骤都会被提升,而普通变量却只能提升声明步骤,而不能提升赋值步骤。
- 变量被提升过后,先对提升上来的所有对象统一执行一遍声明步骤,然后再对变量执行一次赋值步骤。而执行赋值步骤时,会优先执行函数变量的赋值步骤,再执行普通变量的赋值步骤。
先解析,再按顺序执行
例子1
typeof a; // function
function a () {}
var a;
// typeof a; // function =》无论放在前面还是后面,解析后执行顺序都是一样
2
3
4
预编译后
function a // => 声明一个function a
var a // =》 声明一个变量 a
a = () {} // => function a 初始化
typeof a; // function
2
3
4
例子2
function b(){};
var b = 11;
typeof b; // number
2
3
预编译后
function b; // => 声明一个function b
var b; // =》 声明一个变量 b
b = (){}; // =》 function b 初始化
b = 11; // =》 变量 b 初始化 =》变量初始化没有被提升,还在原位
typeof b; // number
2
3
4
5
例子3:结合自执行函数
var foo = 'hello';
(function(foo){
console.log(foo);
var foo = foo || 'world';
console.log(foo);
})(foo);
console.log(foo);
// 依次输出 hello hello hello
2
3
4
5
6
7
8
预编译后
var foo = 'hello';
(function (foo) {
var foo; // undefined;
foo= 'hello'; //传入的foo的值
console.log(foo); // hello
foo = foo || 'world';// 因为foo有值所以没有赋值world
console.log(foo); //hello
})(foo);
console.log(foo);// hello,打印的是var foo = 'hello' 的值(变量作用域)
2
3
4
5
6
7
8
9
10
# var、let、const的区别
1. var 声明的变量会挂载在 window 上,而 let 和 const 声明的变量不会
let 、const 声明的变量会处于当前作用域中<script>
var a = 100;
console.log(window.a); // 100
let b = 100;
console.log(window.b); // undefined
const c = 100;
console.log(window.c); // undefined
console.log(b); // 100 - 当前作用域
2
3
4
5
6
7
8
9
10
11
12
涉及到作用域
2. var 声明变量存在变量提升,let 和 const 不存在变量提升
console.log(a);
var a = 100; // undefined =》变量提升,已声明未赋值,默认undefined
console.log(b);
let b = 100; // Uncaught ReferenceError: Cannot access 'b' before initialization =》 未声明使用,报错
console.log(c);
let c = 100; // Uncaught ReferenceError: Cannot access 'b' before initialization =》 未声明使用,报错
2
3
4
5
6
7
8
9
可以同时关注下【函数提升】有关概念
3. 同一作用域下 var 可以声明同名变量,let和const不能
var a = 100;
console.log(a); // 100
var a = 10;
console.log(a); // 10
let b = 100;
let b = 10; // Uncaught SyntaxError: Identifier 'b' has already been declared
if (true) {
let b = 10;
console.log(b); // 10 => 不同作用域内声明可以
}
2
3
4
5
6
7
8
9
10
11
12
13
虽然 var 可以声明同名变量,但是一般不会这么使用。变量名尽可能是唯一的。可关注下【JS变量命名规范】有关。
4. let 和 const 声明形成块级作用域
if (true) {
var a = 100;
let b = 10;
const c = 10;
}
console.log(a); // 100
console.log(b); // Uncaught ReferenceError: b is not defined
console.log(c); // Uncaught ReferenceError: c is not defined
2
3
4
5
6
7
8
可关注 ES5 是如何模拟块级作用域的
5. 暂时性死区
let/const 存在暂时性死区,var 没有。下面新开标题详解。
6. const
- 一旦声明必须赋值,不能用 null 占位
- 声明一个常量,声明后不能再修改
- 如果声明的是复合类型数据,可以修改其属性
const a = 100;
// a = 200; // Uncaught TypeError: Assignment to constant variable
const list = [];
list[0] = 10;
console.log(list); // [10]
const obj = {a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj); // {a:10000,name:'apple'}
2
3
4
5
6
7
8
9
10
11
12
# 暂时性死区
只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
如果在声明变量或常量之前使用它, 会引发 ReferenceError
, 这在语法上成为 暂存性死区
(temporal dead zone,简称 TDZ)。
由于let、const没有变量提升,才产生了
暂时性死区
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
2
3
4
5
6
7
8
9
10
11
上面代码中,在let命令声明变量tmp之前,都属于变量tmp的“死区”。
在暂时性死区内,typeof
不再是一个百分之百安全的操作
typeof x; // Uncaught ReferenceError: Cannot access 'y' before initialization =》报错:未声明不可用
let x;
typeof undefined_variable // undefined =》未声明的变量不会报错
2
3
4
5
隐蔽型死区
- 与词法作用域结合的暂存死区
function test() {
var foo = 100;
if (true) {
let foo = (foo + 100); // Uncaught ReferenceError: Cannot access 'foo' before initialization
}
}
test();
2
3
4
5
6
7
在 if 语句中,foo 使用 let 进行了声明,此时在 (foo + 100) 中使用的 foo 是 if 语句中的 foo,而不是外面的 var foo = 100; 由于赋值运算符是将右边的值赋予左边,所以先执行了 (foo + 100), 所以 foo 是在还没声明完使用,于是抛出错误。
function team(n) {
console.log(n);
for (let n of n.member) { // Uncaught ReferenceError: Cannot access 'n' before initialization
console.log(n)
}
}
team({member: ['tony', 'lucy']})
2
3
4
5
6
7
8
9
在 for 语句中,n 已经进入了块级作用域,n.member 指向的是 let n ,跟上一例子一样,此时 n 还未声明完,处于暂存死区,故报错。
- switch case中case语句的作用域
switch (x) {
case 0:
let foo;
break;
case 1:
let foo; // TypeError for redeclaration.
break;
}
2
3
4
5
6
7
8
9
会报错是因为switch中只存在一个块级作用域, 改成以下形式可以避免:
let x = 1;
switch(x) {
case 0: {
let foo;
break;
}
case 1: {
let foo;
break;
}
}
2
3
4
5
6
7
8
9
10
11
12
暂时性死区是一个新概念,我们应该保持良好变量声明习惯,尽量避免触发。
参考文章:
https://blog.csdn.net/A13330069275/article/details/81264890
https://www.cnblogs.com/zhaoxiaoying/p/9031890.html
https://es6.ruanyifeng.com/#docs/let
https://www.jianshu.com/p/a90630c41e3e