ECMAScript 和 JavaScript 的关系

ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript)。日常场合,这两个词是可以互换的。

箭头函数

1
2
3
4
5
6
7
// 转码前
input.map(item => item + 1);

// 转码后
input.map(function (item) {
return item + 1;
});

let 和 const 命令

ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

1
2
3
4
5
6
7
{
let a = 10;
var b = 1;
}

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

for循环的计数器,就很合适使用let命令。

1
2
3
4
5
6
for (let i = 0; i < 10; i++) {
// ...
}

console.log(i);
// ReferenceError: i is not defined

let和var的区别

1
2
3
4
5
6
7
8
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
// 这里的i的内存ID不会发生改变(传递的是指针)
a[6](); // 10
1
2
3
4
5
6
7
8
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
// 这里的i的内存ID会发生改变(传递的是值)
a[6](); // 6
  • 第一段代码中,变量ivar命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。

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

    你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

for-let陷进

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

1
2
3
4
5
6
7
8
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// 代码正确运行:
// abc
// abc
// abc

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

let不存在变量提升

“变量提升”现象,即变量可以在声明之前使用,值为undefined

1
2
3
4
5
6
7
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

暂时性死区

在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

1
2
3
4
5
6
var tmp = 123;

if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
1
2
3
4
5
6
7
8
9
10
11
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError

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

tmp = 123;
console.log(tmp); // 123
}
  • 所谓死区,就是只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError
  • 作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。

class

class和传统function的对比

传统function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Person(name,age){
// 实例属性
this.name = name
this.age = age
}

// 静态属性
Person.info = 'aaa'

// 静态方法
Person.show = function(){
console.log('这是Person的静态方法show')
}

// 实例方法
Person.peototype.say = function(){
console.log('这是Person的实例方法say')
}

const p = new Person('hyl',18)
console.log(p)
p.say()
Person.show()

使用class关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Animal{
// 类的构造器
// 就像java一样,如果没有指定构造器,那么就会是一个空的构造器
constructor(name,age){
// 实例属性
this.name = name
this.age = age
}

// 在class内部使用,通过static修饰的属性,就是静态属性
static info = 'aaa'

// 实例方法
say(){
console.log('这是实例方法say')
}

// 静态方法
static show(){
console.log('这是静态方法show')
}
}

const a = new Animal('hyol',18)
console.log(a)
console.log(Animal.info)
a.say()

注意:

  • class的语法块中,只能写构造器,静态方法,实例方法,静态属性
  • class只是语法糖,底层还是使用原来的配方实现的

class继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 因为class只是一个语法糖,所以可以将父类理解成一个 原型对象prototype
class Person{
constructor(name,age){
this.name = name
this.age = age
}

sayHello(){
console.log('say hello')
}
}

class Chinese extends Person {
// 默认调用父类的构造器
}

class American extends Person {
constructor(name,age,gander){
// 子类复写构造器时,第一行必须使用super()调用父类的构造器
super(name,age)
this.gander = gander
}
}

导入

1
2
3
4
5
6
7
8
// CommonJS模块
let { stat, exists, readfile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;