变量


上次更新时间:9/29/2020, 5:41:05 PM 0

变量是存储数据值的容器。

# 变量声明

ES5 有两种声明变量的方法: varfunction

var sum = 0;

function add(a) {
    var sum = a + 1;
    return sum
}
1
2
3
4
5
6

ES6 有六种声明变量的方法:varfunctionletconstimportclass

// ... 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 + ')';
  }
}

1
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;
1
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
1
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 并未被覆盖
1
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 =>全局命名空间搞乱了
1
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 => 理想情况应该输出值 “ 哈哈 ”
1
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();
1
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
1
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
1
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

1
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 

1
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
1
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
1
2
3
4
5
6
7
8

关注执行顺序

# 变量提升与函数提升

js 执行过程:

  1. 词法分析阶段:词法分析主要包括:分析形参、分析变量声明、分析函数声明三个部分。通过词法分析将我们写的js代码转成可以执行的代码,接下来才是执行。
  2. 执行阶段。

一般来说变量和函数会在使用之前就会声明,但是也可以在使用之后再进行声明,这是因为函数声明和变量声明总是会被解释器悄悄地被"提升"到方法体的最顶部。

# 变量提升(Hoisting)

  • 只有声明被提升,初始化不会被提升
  • 声明会被提升到当前作用域的顶端

例子1:

console.log(num); // undefined
var num;
num = 6;
1
2
3

预编译后

var num;
console.log(num); // undefined
num = 6; // 初始化不会被提升
1
2
3

例子2:

num = 6;
console.log(num); // 6 =》 提升了 var sum
var num;
1
2
3

预编译后

var num;
num = 6;
console.log(num); // 6
1
2
3

例子3:

console.log(num); // undefined
var num = 6;
1
2

预编译后

var num;
console.log(num);
num = 6;
1
2
3

例子4:

function hoistVariable() {
  if (!foo) {
    var foo = 5;
  }
  console.log(foo); // 5
}

hoistVariable();
1
2
3
4
5
6
7
8

预编译后

function hoistVariable() {
  var foo     // 将if语句内的声明提升
  if (!foo) { // !undefined = true
    foo = 5;
  }
  console.log(foo); // 5
}

hoistVariable();
1
2
3
4
5
6
7
8
9

例子5:

var foo = 3;
function hoistVariable() {
  var foo = foo || 5;
  console.log(foo); // 5
}
hoistVariable();
1
2
3
4
5
6

预编译后

var foo = 3;
function hoistVariable() {
  var foo
  foo = foo || 5; // 此时 等号右侧 foo 为 undefined
  console.log(foo); // 5
}
hoistVariable();
1
2
3
4
5
6
7

# 函数提升

  • 函数声明和初始化都会被提升
  • 函数表达式不会被提升

例子1:函数声明可被提升

console.log(square(5)); // 25
function square(n) {
  return n * n;
}
1
2
3
4

预编译后

function square = (n) {
  return n * n;
}
console.log(square(5)); // 25
1
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; 
}
1
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; 
}
1
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();
1
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();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 优先级

  • 函数提升在变量提升之前
  1. 变量的问题,莫过于声明和赋值两个步骤,而这两个步骤是分开的。
  2. 函数声明被提升时,声明和赋值两个步骤都会被提升,而普通变量却只能提升声明步骤,而不能提升赋值步骤。
  3. 变量被提升过后,先对提升上来的所有对象统一执行一遍声明步骤,然后再对变量执行一次赋值步骤。而执行赋值步骤时,会优先执行函数变量的赋值步骤,再执行普通变量的赋值步骤

先解析,再按顺序执行

例子1

typeof a; // function
function a () {}
var a;
// typeof a; // function  =》无论放在前面还是后面,解析后执行顺序都是一样
1
2
3
4

预编译后

function a  // => 声明一个function a
var a       // =》 声明一个变量 a
a = () {}   // => function a 初始化
typeof a;   // function
1
2
3
4

例子2

function b(){};
var b = 11;
typeof b; // number
1
2
3

预编译后

function b;  // => 声明一个function b
var b;       // =》 声明一个变量 b
b = (){};    // =》 function b 初始化
b = 11;      // =》 变量 b 初始化 =》变量初始化没有被提升,还在原位
typeof b;    // number
1
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
1
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' 的值(变量作用域)

1
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 -  当前作用域

1
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 =》 未声明使用,报错

1
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 => 不同作用域内声明可以
}

1
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
1
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'}
1
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
}
1
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 =》未声明的变量不会报错

1
2
3
4
5

隐蔽型死区

  1. 与词法作用域结合的暂存死区
function test() {
    var foo = 100;
    if (true) {
        let foo = (foo + 100); //  Uncaught ReferenceError: Cannot access 'foo' before initialization
    }
}
test();
1
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']})
1
2
3
4
5
6
7
8
9

在 for 语句中,n 已经进入了块级作用域,n.member 指向的是 let n ,跟上一例子一样,此时 n 还未声明完,处于暂存死区,故报错。

  1. switch case中case语句的作用域
switch (x) {
  case 0:
    let foo;
    break;
    
  case 1:
    let foo; // TypeError for redeclaration.
    break;
}
1
2
3
4
5
6
7
8
9

会报错是因为switch中只存在一个块级作用域, 改成以下形式可以避免:

let x = 1;

switch(x) {
  case 0: {
    let foo;
    break;
  }  
  case 1: {
    let foo;
    break;
  }
}
1
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

上次更新时间: 9/29/2020, 5:41:05 PM