文章目录
ES6 课程概述
1. ECMAScript、JavaScript、NodeJs,它们的区别是什么?
ECMAScript:简称 ES,是一个语言标准(循环、判断、变量、数组等数据类型)
JavaScript:运行在浏览器端的语言,该语言使用 ES 标准。 ES + web api = JavaScript
NodeJs:运行在服务器端的语言,该语言使用 ES 标准。 ES + node api = JavaScript
无论 JavaScript,还是 NodeJs,它们都是 ES 的超集(super set)
2. ECMAScript 有哪些关键的版本?
ES3.0: 1999
ES5.0: 2009
ES6.0: 2015, 从该版本开始,不再使用数字作为编号,而使用年份
ES7.0: 2016
3. 为什么 ES6 如此重要?
ES6 解决 JS 无法开发大型应用的语言层面的问题。
4. 如何应对兼容性问题?
之后的课程会介绍如何解决
- 学习本课程需要的前置知识有哪些?
HTML+CSS、JavaScript(ES 5)
- 这套课程难不难?
难度和《JavaScript 基础》差不多
声明变量的问题
使用 var 声明变量
-
允许重复的变量声明:导致数据被覆盖
-
变量提升:怪异的数据访问、闭包问题
-
全局变量挂载到全局对象:全局对象成员污染问题
使用 let 声明变量
ES6 不仅引入 let 关键字用于解决变量声明的问题,同时引入了块级作用域的概念
块级作用域:代码执行时遇到花括号,会创建一个块级作用域,花括号结束,销毁块级作用域
声明变量的问题
- 全局变量挂载到全局对象:全局对象成员污染问题
let 声明的变量不会挂载到全局对象
- 允许重复的变量声明:导致数据被覆盖
let 声明的变量,不允许当前作用域范围内重复声明
在块级作用域中用 let 定义的变量,在作用域外不能访问
if (Math.random() < 0.5) {
let a = 123; //定义在当前块级作用域
console.log(a); //当前块级作用域中的a
} else {
//这是另外一个块级作用域,该作用域中找不到a
console.log(a); //a is not defined
}
console.log(a); //a is not defined
- 变量提升:怪异的数据访问、闭包问题
使用 let 不会有变量提升,因此,不能在定义 let 变量之前使用它
底层实现上,let 声明的变量实际上也会有提升,但是,提升后会将其放入到“暂时性死区”,如果访问的变量位于暂时性死区,则会报错:“Cannot access ‘a’ before initialization”。当代码运行到该变量的声明语句时,会将其从暂时性死区中移除。
在循环中,用 let 声明的循环变量,会特殊处理,每次进入循环体,都会开启一个新的作用域,并且将循环变量绑定到该作用域(每次循环,使用的是一个全新的循环变量)
在循环中使用 let 声明的循环变量,在循环结束后会销毁
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
//上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域
//不允许重复声明
//let不允许在相同作用域内,重复声明同一个变量。
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
// Uncaught SyntaxError: Identifier 'a' has already been declared
function func() {
let a = 10;
let a = 1;
}
//因此,不能在函数内部重新声明参数。
// Uncaught SyntaxError: Identifier 'a' has already been declared
function func(arg) {
let arg;
}
func() // 报错
function func(arg) {
{
let arg;
}
}
func() // 不报错
//块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
//ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
// 块级作用域内部的函数声明语句,建议不要使用
{
let a = 'secret';
function f() {
return a;
}
}
// 块级作用域内部,优先使用函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,
const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,
就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
上面代码中,常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
顶层对象的属性
顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。
window.a = 1;
a // 1
a = 2;
window.a // 2
上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。
顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。
这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,
只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);
其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);
最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。
另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。
ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,
依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,
不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
上面代码中,全局变量a由var命令声明,所以它是顶层对象的属性;
全局变量b由let命令声明,所以它不是顶层对象的属性,返回undefined。
默认值
解构赋值允许指定默认值。
let [foo = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。
所以,只有当一个数组成员严格等于undefined,默认值才会生效。
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
function f() {
console.log('aaa');
}
let [x = f()] = [1];
上面代码中,因为x能取到值,所以函数f根本不会执行。上面的代码其实等价于下面的代码。
解构不仅可以用于数组,还可以用于对象。
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;
而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
注意点
(1)如果要将一个已经声明的变量用于解构赋值,必须非常小心。
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。
// 正确的写法
let x;
({x} = {x: 1});
字符串的解构赋值
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
使用 const 声明常量
const 和 let 完全相同,仅在于用 const 声明的变量 ,必须在声明时赋值,而且不可以重新赋值。
实际上,在开发中,应该尽量使用 const 来声明变量,以保证变量的值不会随意篡改,原因如下:
- 根据经验,开发中的很多变量,都是不会更改,也不应该更改的。
- 后续的很多框架或者是第三方 JS 库,都要求数据不可变,使用常量可以一定程度上保证这一点。
注意的细节:
- 常量不可变,是指声明的常量的内存空间不可变,并不保证内存空间中的地址指向的其他空间不可变。
- 常量的命名
- 特殊的常量:该常量从字面意义上,一定是不可变的,比如圆周率、月地距地或其他一些绝不可能变化的配置。通常,该常量的名称全部使用大写,多个单词之间用下划线分割
- 普通的常量:使用和之前一样的命名即可
- 在 for 循环中,循环变量不可以使用常量