变量是存储数据值的容器。
变量声明
ES5 有两种声明变量的方法: var
、function
var sum = 0; function add(a) { var sum = a + 1; return sum }
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 + ')'; } }
student.js
let student = [ { name: 'tony', age: 21, }, { name: 'lucy', age: 18 } ] export default student;
作用域
执行环境
执行环境(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
局部变量是局部作用域,仅在函数体内有用。
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 并未被覆盖
函数体内局部变量优先级高于同名全局变量。同名全局变量会被覆盖。
scope = 'global'; // 声明一个全局变量,可以不用 var 声明 function checkScope2 () { scope = 'local'; // 修改了全局变量 scope myscope = 'local'; // 显式声明了一个新的全局变量 return [scope, myscope]; } checkScope2(); // [local, local] window.scope; // local => 全局变量修改了 window.myscope; // local =>全局命名空间搞乱了
var 声明变量会提升,内部变量可能会覆盖外层变量
var tmp = '哈哈'; function f() { console.log(tmp); if (false) { var tmp = 'hello world'; } } f(); // undefined => 理想情况应该输出值 “ 哈哈 ”
原因在于,预编译后,if 语句内的 temp 声明提升了
var tmp = '哈哈'; function f() { var tmp console.log(tmp); // 打印的是 if 里面提升 temp if (false) { tmp = 'hello world'; } } f();
(变量提升有关请看下面章节介绍)
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
再来一题
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
块级作用域
通过前面的介绍,可以知道,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. 定义函数并传值
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 写法都是利用了 JS 中调用函数传递参数都是值传递的特点
3. 使用setTimeout的第三个参数
for (let k = 0; k < 5; k++) { setTimeout(function () { console.log(k); }, 1000, k); } // 依次输出 0 1 2 3 4
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
关注执行顺序
变量提升与函数提升
js 执行过程:
- 词法分析阶段:词法分析主要包括:分析形参、分析变量声明、分析函数声明三个部分。通过词法分析将我们写的js代码转成可以执行的代码,接下来才是执行。
- 执行阶段。
一般来说变量和函数会在使用之前就会声明,但是也可以在使用之后再进行声明,这是因为函数声明和变量声明总是会被解释器悄悄地被”提升”到方法体的最顶部。
变量提升(Hoisting)
- 只有声明被提升,初始化不会被提升
- 声明会被提升到当前作用域的顶端
例子1:
console.log(num); // undefined var num; num = 6;
预编译后
var num; console.log(num); // undefined num = 6; // 初始化不会被提升
例子2:
num = 6; console.log(num); // 6 =》 提升了 var sum var num;
预编译后
var num; num = 6; console.log(num); // 6
例子3:
console.log(num); // undefined var num = 6;
预编译后
var num; console.log(num); num = 6;
例子4:
function hoistVariable() { if (!foo) { var foo = 5; } console.log(foo); // 5 } hoistVariable();
预编译后
function hoistVariable() { var foo // 将if语句内的声明提升 if (!foo) { // !undefined = true foo = 5; } console.log(foo); // 5 } hoistVariable();
例子5:
var foo = 3; function hoistVariable() { var foo = foo || 5; console.log(foo); // 5 } hoistVariable();
预编译后
var foo = 3; function hoistVariable() { var foo foo = foo || 5; // 此时 等号右侧 foo 为 undefined console.log(foo); // 5 } hoistVariable();
函数提升
- 函数声明和初始化都会被提升
- 函数表达式不会被提升
例子1:函数声明可被提升
console.log(square(5)); // 25 function square(n) { return n * n; }
预编译后
function square = (n) { return n * n; } console.log(square(5)); // 25
例子2:函数表达式不可被提升
console.log(square); // undefined console.log(square(5)); // square is not a function =》 初始化并未提升,此时 square 值为 undefined var square = function (n) { return n * n; }
预编译后
var square console.log(square); // undefined =》赋值没有被提升 console.log(square(5)); // square is not a function =》 square 值为 undefined 故报错 square = function (n) { return n * n; }
例子3:
function hoistFunction() { foo(); // 2 var foo = function() { console.log(1); }; foo(); // 1 function foo() { console.log(2); } foo(); // 1 } hoistFunction();
预编译后
function hoistFunction() { var foo foo = function foo() { console.log(2); } foo(); // 2 foo = function() { console.log(1); }; foo(); // 1 foo(); // 1 } hoistFunction();
优先级
- 函数提升在变量提升之前
- 变量的问题,莫过于声明和赋值两个步骤,而这两个步骤是分开的。
- 函数声明被提升时,声明和赋值两个步骤都会被提升,而普通变量却只能提升声明步骤,而不能提升赋值步骤。
- 变量被提升过后,先对提升上来的所有对象统一执行一遍声明步骤,然后再对变量执行一次赋值步骤。而执行赋值步骤时,会优先执行函数变量的赋值步骤,再执行普通变量的赋值步骤。
先解析,再按顺序执行
例子1
typeof a; // function function a () {} var a; // typeof a; // function =》无论放在前面还是后面,解析后执行顺序都是一样
预编译后
function a // => 声明一个function a var a // =》 声明一个变量 a a = () {} // => function a 初始化 typeof a; // function
例子2
function b(){}; var b = 11; typeof b; // number
预编译后
function b; // => 声明一个function b var b; // =》 声明一个变量 b b = (){}; // =》 function b 初始化 b = 11; // =》 变量 b 初始化 =》变量初始化没有被提升,还在原位 typeof b; // number
例子3:结合自执行函数
var foo = 'hello'; (function(foo){ console.log(foo); var foo = foo || 'world'; console.log(foo); })(foo); console.log(foo); // 依次输出 hello hello hello
预编译后
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' 的值(变量作用域)
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. 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 =》 未声明使用,报错
可以同时关注下【函数提升】有关概念
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 => 不同作用域内声明可以 }
虽然 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
可关注 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'}
暂时性死区
只要块级作用域内存在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 }
上面代码中,在let命令声明变量tmp之前,都属于变量tmp的“死区”。
在暂时性死区内,typeof
不再是一个百分之百安全的操作
typeof x; // Uncaught ReferenceError: Cannot access 'y' before initialization =》报错:未声明不可用 let x; typeof undefined_variable // undefined =》未声明的变量不会报错
隐蔽型死区
- 与词法作用域结合的暂存死区
function test() { var foo = 100; if (true) { let foo = (foo + 100); // Uncaught ReferenceError: Cannot access 'foo' before initialization } } test();
在 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']})
在 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; }
会报错是因为switch中只存在一个块级作用域, 改成以下形式可以避免:
let x = 1; switch(x) { case 0: { let foo; break; } case 1: { let foo; break; } }
暂时性死区是一个新概念,我们应该保持良好变量声明习惯,尽量避免触发。
参考文章: