变量

变量可以不声明就使用

不声明直接使用的弊端,导致全局污染

如果存在其他同名变量则会被其覆盖

  1. function run() {
  2. web = "houdunren";
  3. }
  4. run();
  5. console.log(web); //houdunren

变量提升(向军和阮一峰的例子)

  1. var web = "houdunren";
  2. function hd() {
  3. if (false) {
  4. var web = "后盾人";
  5. }
  6. console.log(web);
  7. }
  8. hd();
  1. var tmp = new Date();
  2. function f() {
  3. console.log(tmp);
  4. if (false) {
  5. var tmp = 'hello world';
  6. }
  7. }
  8. f(); // undefined

var

经典例子

  1. var a = [];
  2. for (var i = 0; i < 10; i++) {
  3. a[i] = function () {
  4. console.log(i);
  5. };
  6. }
  7. a[6](); // 10

代码块

什么是代码块

ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

  1. // 第一种写法,报错
  2. if (true) let x = 1;
  3. // 第二种写法,不报错
  4. if (true) {
  5. let x = 1;
  6. }

上面代码中,第一种写法没有大括号,所以不存在块级作用域,而let只能出现在当前作用域的顶层,所以报错。第二种写法有大括号,所以块级作用域成立。

TDZ

较为隐蔽的TDZ

  1. function bar(x = y, y = 2) {
  2. return [x, y];
  3. }
  4. bar(); // 报错

上面代码中,调用bar函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”。如果y的默认值是x,就不会报错,因为此时x已经声明了。

  1. function bar(x = 2, y = x) {
  2. return [x, y];
  3. }
  4. bar(); // [2, 2]

另外,下面的代码也会报错,与var的行为不同。

  1. // 不报错
  2. var x = x;
  3. // 报错
  4. let x = x;
  5. // ReferenceError: x is not defined

上面代码报错,也是因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“。

TDZ的本质

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

重点理解:当前作用域

const的作用域与let命令相同:只在声明所在的块级作用域内有效。

let

使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。

  1. var a = [];
  2. for (let i = 0; i < 10; i++) {
  3. a[i] = function () {
  4. console.log(i);
  5. };
  6. }
  7. a[6](); // 6

上面代码中,变量ilet声明的,当前的i只在本轮循环有效,所以每一次循环的<font style="color:#F5222D;">i</font>其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

重点理解

另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

  1. for (let i = 0; i < 3; i++) {
  2. let i = 'abc';
  3. console.log(i);
  4. }
  5. // abc
  6. // abc
  7. // abc

上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。

结合方应杭文章

https://zhuanlan.zhihu.com/p/28140450

块级作用域与函数声明

const

常量名建议全部大写 声明时必须同时赋值 可以修改引用类型变量的值 改变常量的引用类型值
  1. const INFO = {
  2. url: 'https://www.houdunren.com',
  3. port: '8080'
  4. };
  5. INFO.port = '443';
  6. console.log(INFO);

下面演示了在不同作用域中可以重名定义常量

  1. const NAME = '后盾人';
  2. function show() {
  3. const NAME = '向军大叔';
  4. return NAME;
  5. }
  6. console.log(show());
  7. console.log(NAME);

let,const在不同作用域可以重新声明

  1. let web = 'houdunren.com';
  2. if (true) {
  3. let web = '后盾人'; //Identifier 'web' has already been declared
  4. }
  1. const NAME = '后盾人';
  2. function show() {
  3. const NAME = '向军大叔';
  4. return NAME;
  5. }
  6. console.log(show());
  7. console.log(NAME);

let在全局声明的变量不会存在于window对象中

  1. let hd = "hdcms";
  2. console.log(window.hd); //undefined

对比var

window全局对象污染与重复声明

  1. var x = 1;
  2. console.log(window.x); // 1
  3. // 如果不小心声明为已存在的属性
  4. var screenLeft = 2;
  5. console.log(window.screenLeft); // 2,原意失效
  6. let screenLeft = 3;
  7. console.log(window.screenLeft);
  8. console.log(screenLeft);

let相比var,避免了全局污染

const本质

<font style="color:#096DD9;">const</font>实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,<font style="color:#096DD9;">const</font>只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

!!!并不是直接可以修改const声明的对象类型
  1. const foo = {};
  2. // 为 foo 添加一个属性,可以成功
  3. foo.prop = 123;
  4. foo.prop // 123
  5. // 将 foo 指向另一个对象,就会报错
  6. foo = {}; // TypeError: "foo" is read-only

上面代码中,常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

下面是另一个例子。

  1. const a = [];
  2. a.push('Hello'); // 可执行
  3. a.length = 0; // 可执行
  4. a = ['Dave']; // 报错

上面代码中,常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。

思考:const声明的对象

进一步思考

地址类型引用(如声明对象)可以更改

存在弊端

有些时候不想被修改

使用Object的静态方法Object.freeze()

  1. const foo = Object.freeze({});
  2. // 常规模式时,下面一行不起作用;
  3. // 严格模式时,该行会报错
  4. foo.prop = 123;

上面代码中,常量foo指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

  1. var constantize = (obj) => {
  2. Object.freeze(obj);
  3. Object.keys(obj).forEach( (key, i) => {
  4. if ( typeof obj[key] === 'object' ) {
  5. constantize( obj[key] );
  6. }
  7. });
  8. };

ES6 声明变量的六种方法

ES5 只有两种声明变量的方法:var命令和function命令。ES6 除了添加letconst命令,后面章节还会提到,另外两种声明变量的方法:import命令和class命令。所以,ES6 一共有 6 种声明变量的方法。

顶层对象的属性

为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
  1. var a = 1;
  2. // 如果在 Node 的 REPL 环境,可以写成 global.a
  3. // 或者采用通用方法,写成 this.a
  4. window.a // 1
  5. let b = 1;
  6. window.b // undefined

上面代码中,全局变量avar命令声明,所以它是顶层对象的属性;全局变量blet命令声明,所以它不是顶层对象的属性,返回undefined

规范

块级作用域

(1)let 取代 var

ES6 提出了两个新的声明变量的命令:letconst。其中,let完全可以取代var,因为两者语义相同,而且let没有副作用。

  1. 'use strict';
  2. if (true) {
  3. let x = 'hello';
  4. }
  5. for (let i = 0; i < 10; i++) {
  6. console.log(i);
  7. }

上面代码如果用var替代let,实际上就声明了两个全局变量,这显然不是本意。变量应该只在其声明的代码块内有效,var命令做不到这一点。

var命令存在变量提升效用,let命令没有这个问题。

  1. 'use strict';
  2. if (true) {
  3. console.log(x); // ReferenceError
  4. let x = 'hello';
  5. }

上面代码如果使用var替代letconsole.log那一行就不会报错,而是会输出undefined,因为变量声明提升到代码块的头部。这违反了变量先声明后使用的原则。

所以,建议不再使用var命令,而是使用let命令取代。

(2)全局常量和线程安全

letconst之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。

const优于let有几个原因。一个是const可以提醒阅读程序的人,这个变量不应该改变;另一个是const比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对const进行优化,所以多使用const,有利于提高程序的运行效率,也就是说letconst的本质区别,其实是编译器内部的处理不同。

  1. // bad
  2. var a = 1, b = 2, c = 3;
  3. // good
  4. const a = 1;
  5. const b = 2;
  6. const c = 3;
  7. // best
  8. const [a, b, c] = [1, 2, 3];

const声明常量还有两个好处,一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。

所有的函数都应该设置为常量。

长远来看,JavaScript 可能会有多线程的实现(比如 Intel 公司的 River Trail 那一类的项目),这时let表示的变量,只应出现在单线程运行的代码中,不能是多线程共享的,这样有利于保证线程安全。