【ES6脚丫系列】let+const+变量+变量作用域+块作用域+变量声明提升

243 阅读15分钟

【ES6脚丫系列】let+const+变量+变量作用域+块作用域+变量声明提升


本文字符数9600+,阅读时间约20分钟。


一、let命令


【01】ES6新加。

【02】let用于声明变量,let声明的变量,只在let命令所在的代码块内有效。
所谓的代码块就是指花括号内的东西。

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

【03】在for循环的小括号里,let定义的变量,只在for循环体内有效。其他地方就是未定义了。
例子:
for(let i = 0; i < arr.length; i++){}
console.log(i)//ReferenceError: i is not defined


例子:
下面的代码for使用var,最后输出的是10。因为i在全局作用域内有效,。所以每一次循环,新的i值都会覆盖旧值,导致最后输出的是最后一轮的i的值。

/*
zyx456解释:a[i]中的i是每次for循环的i值,此时function(){console.log(i);})并没有运行。
a[6]();运行时,才开始获取i,此时i是最后for循环后的值,也就是10。
*/
var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10


如果使用let,声明的变量仅在块级作用域内有效,最后输出的是6。
var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6 

上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。




【04】不存在变量声明提升,只能先声明,再使用。否则报错。
console.log(foo); // ReferenceError
let foo = 2;


这也意味着typeof不再是一个百分之百安全的操作。
由于typeof运行时,x还没有声明,所以会抛出一个ReferenceError。
typeof x; // ReferenceError
let x;




【05】暂时性死区(temporal dead zone,简称TDZ)

意思就是:如果代码块内使用let声明了某个变量x,那么在这个声明之前,这个变量不能被赋值或使用。否则就会报错。

打个比方,你的朋友胖虎有个变形金刚,在他说给你玩之前,你不能抢过来玩,更不能卖掉。

其实,就是为了规范,避免先使用后声明这种奇葩的行为。

【】例子:
var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
} 

【】例子:
if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}


【】有些“死区”比较隐蔽,不太容易发现。
function bar(x = y, y = 2) {
  return [x, y];
}

bar(); // 报错

【】例子:
function bar(x = 2, y = x) {
  return [x, y];
}
bar(); // [2, 2]





【05】不能重复声明
用let声明后,不可以再用let或var声明同一个变量。
不能在函数内部用let声明函数形参。

// 报错
function () {
  let a = 10;
  var a = 1;
}

// 报错
function () {
  let a = 10;
  let a = 1;
}


function func(arg) {
  let arg; // 报错
}

function func(arg) {
  {
    let arg; // 不报错
  }
}


~~~~~~~~~~~~~~~~~~~~~ 我是zyx456的分割线 ~~~~~~~~~~~~~~~~~~~~~~~~

二、const命令



【01】声明的是常量,一旦声明,不得修改。所以必须声明的同时并赋值。否则报错。

const的定义是不可重新赋值的值,与不可变的值(immutable value)不同。重新赋值会报错。

如果const定义的是引用类型,比如Object,在定义之后仍可以修改属性。


它的使用场景很广,包括常量、配置项以及引用的组件、定义的 “大部分” 中间变量等,都应该以const做定义。

let多用于在 loop循环(for,while)和少量必须重赋值的变量上。




const PI = 3.1415;
PI // 3.1415

PI = 3;// TypeError: "PI" is read-only
const foo;// SyntaxError: missing = in const declaration



和let一样:

【02】只在声明所在的块级作用域内有效。

if (true) { const MAX = 5;} MAX // Uncaught ReferenceError: MAX is not defined

【03】const命令声明的常量也是不提升。
【04】同样存在暂时性死区,只能在声明的位置后面使用。
if (true) {
  console.log(MAX); // ReferenceError
  const MAX = 5;} 


【05】const声明的常量,也与let一样不可重复声明。
var message = "Hello!";let age = 25;
// 以下两行都会报错
const message = "Goodbye!";
const age = 30;






【06】对于引用类型的变量,变量名不指向数据,而是指向数据所在的地址。(指针的概念。)
const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。
例子:
const foo = {};
foo.prop = 123;

foo.prop// 123

foo = {} // TypeError: "foo" is read-only不起作用

例子:
const a = [];
a.push("Hello"); // 可执行
a.length = 0;    // 可执行
a = ["Dave"];    // 报错



【07】ES5只有两种声明变量的方式:var命令和function命令。
ES6除了添加let和const命令,另外两种声明变量的方法:import命令和class命令。
所以,ES6一共有6种声明变量的方法。

【08】ES6规定,var命令和function命令声明的全局变量,依旧是全局对象的属性;
let命令、const命令、class命令声明的全局变量,不属于全局对象的属性。

var a = 1;
// 如果在Node的REPL环境,可以写成global.a
// 或者采用通用方法,写成this.a
window.a // 1
let b = 1;
window.b // undefined



【09】如果真的想将对象冻结不再变,应该使用Object.freeze()方法。此时添加属性无效。
const foo = Object.freeze({});
foo.prop = 123; // 不起作用



除了将对象本身冻结,对象的属性也应该冻结。
下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
	Object.freeze(obj);
	Object.keys(obj).forEach((key, value) => {
		if (typeof obj[key] === 'object') {
			constantize(obj[key]);
		}
	});
};



【10】跨模块常量
const声明的常量只在当前代码块有效。
如果想设置跨模块的常量,可以将这些变量暴露出去,提供给其他模块即可。
// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3

// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3

【01】变量+标识符
06、在函数内部,没有var声明的变量,是作为全局变量存在的;
当用var声明一个JS全局变量时,实际上是定义了全局对象window的一个属性。


~~~~~~~~~~~~~ 我是晚上没盖被子的分割线 ~~~~~~~~~~~~~~~~~~~



三、变量+标识符


【01】声明变量
01、变量是用var运算符(variable 的缩写)加变量名定义的。
例如:
var test = "hi";


02、ES中的变量并不一定要初始化赋值。
如果未在var声明语句中给变量指定初始值,那么它的初始值就是undefined。var test;

03、可以用一个 var 语句定义两个或多个变量:
var test1 = "hi", test2 = "hello";


04、变量可以是不同类型。
var test = "hi", age = 25;

05、由于由于ES是弱类型的,所以变量声明时不用声明类型。
变量可以存放不同类型的值。这是弱类型变量的优势。
变量是弱类型的。或者说是“松散类型”。就是可以用来保存任何类型的数据。
可以随时改变变量保存的数据的类型。
例如,可以把变量初始化为字符串类型的值,之后把它设置为数字值,如下所示:
var test = "hi";
alert(test);
test = 55;
alert(test);


06、在函数内部,没有var声明的变量,是作为全局变量存在的;


【02】变量的生命周期
JS变量的生命期从它们被声明的时间开始。
局部变量会在函数运行以后被删除。
全局变量会在页面关闭后被删除。


【03】当用var声明一个JS全局变量时,实际上是定义了全局对象window的一个属性。

【04】当使用var声明一个变量时,这个变量无法通过delete运算符删除。
非严格模式,给一个未声明的变量赋值,JS会自动创建一个全局变量,可以用delete删除。
例子:
var truevar = 1;        // 声明一个不可删除的全局变量
fakevar = 2;            // 创建全局对象的一个可删除的属性
this.fakevar2 = 3;      // 同上
delete truevar          // => false: 变量并没有被删除
delete fakevar          // => true: 变量被删除
delete this.fakevar2    // => true: 变量被删除



【05】声明、初始化、赋值之间的区别

声明: 变量将会对一个包含相应作用域范围(比如: 在一个函数里面)的名字进行注册。

初始化: 当你声明一个变量的时候会自动的进行初始化,也就是说由 JavaScript 解释引擎为变量分配了一块内存。

赋值: 把一个指定的值指派(赋)给一个变量。

使用:使用变量的值 例如alert(myValue)



~~~~~~~~~~~~~ 我是被蚊子咬的分割线 ~~~~~~~~~~~~~~~~~~~

四、变量作用域



【01】全局变量和局部变量

全局变量
在函数外声明的变量是全局变量,网页上的所有脚本和函数都能访问它。

局部变量
在JS函数内部声明的变量(使用 var)是局部变量,只能在函数内部访问它。(该变量的作用域是局部的)。 可以在不同的函数中使用名称相同的局部变量,因为只有声明过该变量的函数才能识别出该变量。 只要函数运行完毕,局部变量就会被删除。
function a (){ var b=3; }
console.log(b);//b is not defined


【02】一个变量的作用域(scope)是代码中定义这个变量的区域。
全局变量拥有全局作用域,在JS代码中的任何地方都是有定义的。
然而在函数内声明的变量只在函数体内有定义。
它们是局部变量,作用域是局部性的。
函数参数也是局部变量,它们只在函数体内有定义。


【03】在ES5中,JS通过函数管理作用域。ES6中,有块作用域。


【04】全局变量会在下列情况下出现:
  • 在任何地方不使用 var ,let,const声明变量。
  • 直接向未声明的变量赋值。
  • 在函数外部使用 var 声明的变量。
  • 以 window. variable 形式声明的变量。


【05】全局变量的问题
所有的JS文件共享这些全局变量,它们在同一个全局命名空间,很容易命名冲突。
比如:
  • 第三方的功能库文件。
  • 同事的代码。
  • 第三方的用户分析代码。

【06】以下是几种常见的误将局部变量声明为全局变量的例子。

01、函数内部不使用 var 声明变量
function sum(x, y) {
   // 不推荐写法: 隐式全局变量
   result = x + y;
   return result;
}


02、使用任务链进行部分 var 声明
a是本地变量但是b确实全局变量。赋值表达式是从右向左进行的。
等效于: var a = (b = 0);
// 反例,勿使用
function foo() {
   var a = b = 0;
   // ...
}


【07】避免全局变量的方式:
方式1:使用ES6的let和const声明变量。
方式2:使用立即自执行函数。
(val=>val+1)(3); //4






【08】在函数体内,局部变量的优先级高于同名的全局变量。
如果在函数内声明的一个局部变量或者函数参数中带有的变量和全局变量重名,那么全局变量就被局部变量所覆盖。

var scope = "global";           // 声明一个全局变量
function checkscope() {
        var scope = "local";    // 声明一个同名的局部变量
        return scope;           // 返回局部变量的值,而不是全局变量的值
}
checkscope()                    // => "local"


【09】函数定义是可以嵌套的。由于每个函数都有它自己的作用域,因此会出现几个局部作用域嵌套的情况,例如:
var scope = "global scope";     // 全局变量
function checkscope() {
    var scope = "local scope";      //局部变量 
    function nested() {
        var scope = "nested scope";     // 嵌套作用域内的局部变量
        return scope;                   // 返回当前作用域内的值
    }

    return nested();
}

checkscope()                    // => "嵌套作用域"



【10】函数中用var声明的所有变量,会出现变量声明提升的现象,就是可以在变量声明前使用它,但是,只是声明提升到函数的顶部,变量的值并没有跟着。在赋值前,变量的值为undefined。

var a = 2;
function test(){
    console.log(a);
    var a = 10;
}
test();//undefined;

【11】词法作用域和动态作用域

01、在程序设计语言中,变量可分为自由变量与约束变量两种。

简单来说,局部变量和参数都被认为是约束变量;

而不是约束变量的则是自由变量。


02、在冯·诺依曼计算机体系结构的内存中,变量的属性可以视为一个六元组:(名字,地址,值,类型,生命期,作用域)。


地址属性具有明显的冯·诺依曼体系结构的色彩,代表变量所关联的存储器地址(内存)。

类型规定了变量的取值范围和可能的操作。(比如数值有取值范围)

生命期表示变量与某个存储区地址绑定的过程。

根据生命期的不同,变量可以被分为四类:静态、栈动态、显式堆动态和隐式堆动态。


作用域限制了变量在语句中的适用范围,分为词法作用域和动态作用域两种。


在词法作用域的环境中,变量的作用域与其在代码中所处的位置有关。

由于代码可以静态决定(运行前就可以决定),所以变量的作用域也可以被静态决定,因此也将该作用域称为静态作用域。

在动态作用域的环境中,变量的作用域与代码的执行顺序有关。


JavaScript和C都是词法作用域语言。



~~~~~~~~~~~~~ 我是晚上没盖被子的分割线 ~~~~~~~~~~~~~~~~~~~

五、块级作用域


【01】为什么要有块级作用域?
ES5只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。


01、内层变量覆盖外层变量,因为存在变量声明提升和函数声明提升。
函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。
var tmp = new Date();
function f(){
  console.log(tmp);
  if (false){
    var tmp = "hello world";
  }
}
f() // undefined




02、用来计数的循环变量泄露为全局变量。循环结束后,它并没有消失,泄露成了全局变量。
var s = 'hello';
for (var i = 0; i < s.length; i++){
  console.log(s[i]);
}
console.log(i); // 5





【02】ES6的块级作用域
let和const为JavaScript新增了块级作用域。
function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}





【03】内层作用域可以和外层作用域定义同名的变量。
{{{{let insane = 'Hello World';{let insane = 'Hello World';}}}}};

【04】立即执行匿名函数(IIFE)不再必要了。

// IIFE写法
(function () {var tmp = ...;...}());
// 块级作用域写法
{let tmp = ...;...}
【05】函数的作用域,在其定义的父级代码块作用域之内。

function f() {
	console.log('I am outside!');
}
(function () {
	if (false) { // 重复声明一次函数f
		function f() {
			console.log('I am inside!');
		}
	}
	f();
}());


上面代码在ES5中运行,会得到“I am inside!”,但是在ES6中运行,会得到“I am outside!”。
这是因为ES5存在函数提升,不管会不会进入 if代码块,函数声明都会提升到当前作用域的顶部,得到执行;
而ES6支持块级作用域,不管会不会进入if代码块,其内部声明的函数皆不会影响到作用域的外部。


【06】块级作用域外部,无法调用块级作用域内部定义的函数。如果确实需要调用,就要像下面这样处理。
{
  let a = 'secret';
  function f() {
    return a;
  }
}
f() // 报错
【】例子:
let f;{let a = 'secret';
  f = function () {return a;}}f(); // "secret"




【07】在严格模式下,函数只能在顶层作用域和函数内声明,其他情况(比如if代码块、循环代码块)的声明都会报错。


【08】ES6允许块级作用域的任意嵌套。

{{{{{let insane = 'Hello World'}}}}};

上面代码使用了一个五层的块级作用域。外层作用域无法读取内层作用域的变量。
{{{{{let insane = 'Hello World'}
  console.log(insane); // 报错
}}}};


~~~~~~~~~~~~~ 我是晚上没盖被子的分割线 ~~~~~~~~~~~~~~~~~~~

六、变量和函数声明提升



【01】变量提升(hoisting)是 JavaScript 编译器的行为,将所有的变量和函数声明移至当前作用域的最高处。

然而,只有声明被提升了。任何赋值行为都被留在它们所在的地方。

如果在声明之前访问该变量,它的值是undefined。


例子:

(function() {
  var foo = 1;
  var bar = 2;
  var baz = 3;

  console.log(foo + " " + bar + " " + baz);//"1 2 3"
})();

【】例子:

(function() {
  var foo = 1;
  console.log(foo + " " + bar + " " + baz);// "1 undefined undefined"
  var bar = 2;
  var baz = 3;
})();


【】例子:

(function() {
  var foo = 1;
  console.log(foo + " " + bar + " " + baz);// ReferenceError baz未定义。
  var bar = 2;


【02】函数变量提升(hoisting)

函数声明也是可以被提升的。

函数声明表达式不会声明提升。

【】例子:

foo();//Hello! 成功。

function foo() {
  console.log("Hello!");
}

【】例子:

foo();//失败。函数表达式声明不能函数提升。

var foo = function() {
  console.log("Hello!");
};
【03】声明命令和变量声明提升
用var 声明的变量存在变量声明提升。
let和const声明的变量不会变量声明提升,存在暂时性死区。
class 声明的类不会声明提升。

function double(num) {
  console.log(myVariable); // => undefined
  var myVariable;
  return num * 2;
}
double(3); // => 6