ECMAScript常用语法整理

首先集百家之介绍:

ECMAScript 是一种由 Ecma 国际(前身为欧洲计算机制造商协会)通过 ECMA-262 标准化的脚本程序设计语言。这种语言在万维网上应用广泛,它往往被称为 JavaScript 或 JScript,但实际上后两者是 ECMA-262 标准的实现和扩展。

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

目前 ES 已经到达了 ES2018 版本,Google 的 V8 引擎支持率 100%,其他的并不友好,而我们常用 JavaScript 稳定版本的实现目前在 ES2016 版本,所以这里主要学习 ES6 的特性了。
如果真的有什么原因不能使用 ES6 可以使用 Babel 将 ES6 语法转为 ES5.
我会把实际中频繁用到的一些特性写出来,致力于用最优雅的写法写出更高质量的代码。

let和const

使用 let 声明的变量只在它所在的代码块内有效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
let a = 10;
var b = 1;
}

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

var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6,使用 var 则都是 10

例如 for 循环就合适使用 let 定义 i

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

var 命令会发生“变量提升”现象,即变量可以在声明之前使用,值为 undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
为了纠正这种现象,let 命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

ES6 明确规定,如果区块中存在 letconst 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
ES6 规定,块级作用域之中,函数声明语句的行为类似于 let,在块级作用域之外不可引用。

总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ),不过应该提倡能用 let 的时候尽量别用 var,避免造成作用域的混乱。


const 声明一个只读的常量。一旦声明,常量的值就不能改变。
这意味着,const 一旦声明变量,就必须立即初始化,不能留到以后赋值。
const的作用域与let命令相同:只在声明所在的块级作用域内有效,不提升、存在暂时性死区。

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

字符串

在 ES6 中,对字符串进行了增强,尤其是模板字符串,真是非常的好用!

模板字符串

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量或者调用函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
not legal.`

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

// 模板字符串之中还能调用函数
function fn() {
return "Hello World";
}
`foo ${fn()} bar`
// foo Hello World bar

如果模板字符串中的变量没有声明,将报错。如果大括号中的值不是字符串,将按照一般的规则(toString)转为字符串。

新增方法

ES5 字符串的实例方法很有限,基本就是 indexOf 了,在 ES6 新加入了一些:

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
  • repeat():返回一个新字符串,表示将原字符串重复 n 次。

在 ES2017 和 ES2019 又引入了 padStart() 用于头部补全,padEnd() 用于尾部补全和 trimStart()trimEnd() 这两个方法。

函数

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function log(x, y = 'World') {
console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello


// 与解构赋值默认值结合使用
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined


// 如果没有提供参数,函数 foo 的参数默认为一个空对象
function foo({x, y = 5} = {}) {
console.log(x, y);
}
foo() // undefined 5

ES6 引入 rest 参数(形式为 ...变量名),用于获取函数的多余参数,本质是个数组,跟 Java 很类似:

1
2
3
4
5
6
7
8
9
10
11
function add(...values) {
let sum = 0;

for (var val of values) {
sum += val;
}

return sum;
}

add(2, 5, 3) // 10

其次还有函数的 name 属性,返回该函数的函数名。

箭头函数

ES6 允许使用“箭头”(=>)定义函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var f = v => v;
// 等同于
var f = function (v) {
return v;
};

var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};

// 正常函数写法
[1,2,3].map(function (x) {
return x * x;
});

// 箭头函数写法
[1,2,3].map(x => x * x);

怎么说呢,这个其实就是简化的匿名函数,用在回调的地方非常好用。箭头函数有几个使用注意点。

  1. 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。
  2. 不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误。
  3. 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  4. 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

其中第一点尤其值得注意。this 对象的指向是可变的,但是在箭头函数中,它是固定的

1
2
3
4
5
6
7
8
9
10
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

关于 this 的这个问题,版本对比为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ES6 版本
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}

// ES5 版本
function foo() {
var _this = this;

setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}

// 不适用情况
// 对象不构成单独的作用域,导致 jumps 箭头函数定义时的作用域就是全局作用域。
const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}

实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。
在 Vue 很多使用中,如果你使用箭头函数就不需要再在尾部来一个 .bind(this) 了。

其他补充

使用 JSON.stringify() 方法可以将对象转为字符串类型的 json 格式。


关于 apply 和 call ,ECMAScript 规范给所有函数都定义了 call 与 apply 两个方法,它们的应用非常广泛,它们的作用也是一模一样,只是传参的形式有区别而已。
apply 方法传入两个参数:一个是作为函数上下文的对象,另外一个是作为函数参数所组成的数组。
call 方法第一个参数也是作为函数上下文的对象,但是后面传入的是一个参数列表,而不是单个数组。
一般来说,它们的作用就是改变 this 的指向,或者借用别等对象的方法,那么它和 bind 什么区别呢?

在 EcmaScript5 中扩展了叫 bind 的方法,在低版本的 IE 中不兼容。
它和 call 很相似,接受的参数有两部分,第一个参数是是作为函数上下文的对象,第二部分参数是个列表,可以接受多个参数。

他们的主要区别就是:

bind 方法不会立即执行,而是返回一个改变了上下文 this 后的函数。而原函数 func 中的 this 并没有被改变,依旧指向全局对象 window。
在参数传递上,也有一些区别,看个例子:

1
2
3
4
5
6
7
8
9
function func(a, b, c) {
console.log(a, b, c);
}
var func1 = func.bind(null,'linxin');

func('A', 'B', 'C'); // A B C
func1('A', 'B', 'C'); // linxin A B
func1('B', 'C'); // linxin B C
func.call(null, 'linxin'); // linxin undefined undefined

call 是把第二个及以后的参数作为 func 方法的实参传进去,而 func1 方法的实参实则是在 bind 中参数的基础上再往后排

数组的扩展

扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列

1
2
3
4
5
6
7
8
console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

对于数组的克隆与合并,有了扩展运算符也变得简单多了:

1
2
3
4
5
6
7
8
9
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

ES5 中只能使用 concat 函数间接达到目的。
字符串也可以被展开:[...'hello'],还可以用于 Generator 函数:

1
2
3
4
5
6
7
const go = function*(){
yield 1;
yield 2;
yield 3;
};

[...go()] // [1, 2, 3]

对象扩展

现在对象的属性有了更简洁的写法:

1
2
3
4
5
6
7
8
9
10
11
const baz = {foo};
// 等同于
const baz = {foo: foo};

function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}

简单说就是当 key 和 val 一样时,可以进行简写。其实,方法也可以进行简写:

1
2
3
4
5
6
7
8
9
10
11
12
const o = {
method() {
return "Hello!";
}
};

// 等同于
const o = {
method: function() {
return "Hello!";
}
};

这种写法会非常的简洁,另外常用的还有 setter 和 getter,就是采用的这种方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const cart = {
_wheels: 4,

get wheels () {
return this._wheels;
},

set wheels (value) {
if (value < this._wheels) {
throw new Error('数值太小了!');
}
this._wheels = value;
}
}

需要注意的一点就是简洁写法的属性名总是字符串。在对象定义上,也变得更加灵活了:

1
2
3
4
5
6
let propKey = 'foo';

let obj = {
[propKey]: true,
['a' + 'bc']: 123
};

ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象


另外,对象也有扩展运算符,例如:

1
2
3
4
5
6
7
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

var ll = {name:'loli', age: 12, getVal(val){console.log(val)}}
var test = {...User, dd:'dd'}
test.getVal(test.dd)

简单说就是把对象里的方法进行拷贝,Vuex 中的这种写法算是明白了吧,Vuex 中,我们经常用类似 ...mapState({xxx}) 的写法,很显然 mapState 函数返回的是一个对象,然后我们使用“展开运算符”将其展开了。

遍历

变量数组或者对象,可以使用 forEach 这个函数(ES5 中也可使用):

1
2
3
4
5
6
7
[1, 2 ,3, 4].forEach(alert);

[1, 2 ,3, 4].forEach((item, index) => {console.log(item)})

arr.forEach(function callback(currentValue, index, array) {
//your iterator
}[, thisArg]);

使用 forEach 函数进行遍历时,中途无法跳过或者退出;
在 forEach 中的 return、break、continue 是无效的。

see:https://www.jianshu.com/p/bdf77ee23089

然后遍历除了基本的 fori,还有两种:for...infor...of ,那么他们俩有啥区别呢?

  1. 推荐在循环对象属性的时候,使用 for...in,在遍历数组的时候的时候使用 for...of
  2. for...in 循环出的是 key,for...of 循环出的是 value
  3. 注意,for...of 是 ES6 新引入的特性。修复了 ES5 引入的 for...in 的不足
  4. for...of 不能循环普通的对象,需要通过和 Object.keys() 搭配使用

下面是一段示例代码:

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
28
29
30
let aArray = ['a',123,{a:'1',b:'2'}]

for(let index in aArray){
console.log(`${aArray[index]}`);
}
// 结果:
// a
// 123
// [object Object]


for(let value of aArray){
console.log(value);
}
// 结果:
// a
// 123
// {a: "1", b: "2"}


// 可以使用 for...of 遍历 Map,它部署了 Iterator 接口
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
console.log(key + " is " + value);
}
// first is hello
// second is world

作用于数组的 for-in 循环除了遍历数组元素以外,还会遍历自定义属性。
for...of 循环不会循环对象的 key,只会循环出数组的 value,因此 for...of 不能循环遍历普通对象,对普通对象的属性遍历推荐使用 for...in

reduce

某次,遇到一个做累加的需求,用传统的方式肯定是没问题,但是我想到既然是动态语言,就没有什么骚操作?
结果搜了一下,确实有很多骚操作,还有直接用 eval 黑魔法的,不过,我觉得比较优雅的就是 reduce 方法了:

1
2
var arr = [1,2,3]
arr.reduce((prev, cur) => prev + cur, 0)

总感觉似曾相识,不知道在哪里用过,也许是 J8 的 Lambda 吧,这样看来 reduce 可以做的东西就多了。

forEach与map

MDN 上的描述:

forEach():针对每一个元素执行提供的函数 (executes a provided function once for each array element)。

map()创建一个新的数组,其中每一个元素由调用数组中的每一个元素执行提供的函数得来 (creates a new array with the results of calling a provided function on every element in the calling array)。

forEach 方法不会返回执行结果,而是 undefined。也就是说,forEach() 会修改原来的数组。而 map() 方法会得到一个新的数组并返回。

1
2
3
4
5
6
7
8
9
10
11
12
// 将数组中的数据翻倍
let arr = [1, 2, 3, 4, 5];

arr.forEach((num, index) => {
return (arr[index] = num * 2);
});

let doubled = arr.map(num => {
return num * 2;
});

// 结果都为: [2, 4, 6, 8, 10]

如果你习惯使用函数是编程,那么肯定喜欢使用 map()。因为 forEach() 会改变原始的数组的值,而 map() 会返回一个全新的数组,原本的数组不受到影响。
总之,能用forEach()做到的,map()同样可以。反过来也是如此。
一般来说,使用 map 速度会更快,测试地址:https://jsperf.com/map-vs-foreach-speed-test

Class语法

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。
基本上,ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 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
// ES5
function Point(x, y) {
this.x = x;
this.y = y;
}

Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);


// ES6
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}

toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}

对于做静态语言后端的我,果然还是 ES6 的写法更舒服。

定义“类”的方法的时候,前面不需要加上 function 这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

既然说 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
28
29
30
31
32
33
34
35
36
37
38
class Point {
// ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

/*******************分割线********************/

class Point {
constructor() {
// ...
}

toString() {
// ...
}

toValue() {
// ...
}
}

// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};


class B {}
let b = new B();
b.constructor === B.prototype.constructor // true

Point.prototype.constructor === Point // true

Point.name // "Point"

类的数据类型就是函数,类本身就指向构造函数。
构造函数的 prototype 属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的 prototype 属性上面。
类的内部所有定义的方法,都是不可枚举的(non-enumerable),这一点与 ES5 的行为不一致。
生成实例对象如果忘记加上 new,像函数那样调用 Class,将会报错。
类不存在变量提升(hoist),也就是没办法先使用后定义。
此外还有很多需要注意的点,不过我认为我知道这一部分就足够了,了解更多就去看阮一峰的书吧。

Module语法

历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的 require、Python 的 import,甚至就连 CSS 都有 @import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

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;

上面代码的实质是整体加载 fs 模块(即加载 fs 的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,再通过 import 命令输入。

1
2
3
// ES6模块
// 仅加载三个方法(函数)
import { stat, exists, readFile } from 'fs';

这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

export

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量。
下面展示一下几种 export 的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 第一种
export var firstName = 'Michael';
export var year = 1958;

// 第二种(推荐)
var firstName = 'Michael';
var year = 1958;

export { firstName, year };

// 输出函数或者类或者对象
export function multiply(x, y) {
return x * y;
};
// 输出函数自定义名称
function v2() { ... }
export {
v2 as streamV2,
v2 as streamLatestVersion
};

它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系,所以你不能直接输出一个值,例如数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 报错
export 1;
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};


// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};

export 命令可以出现在模块的任何位置,只要处于模块顶层就可以。

import

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 自定义函数名
import { lastName as surname } from './profile.js';

import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
a.foo = 'hello'; // 合法操作(非常不建议)

// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;

// 整体导入
// 通过 别名.函数名 调用
import * as circle from './circle';

import 命令输入的变量都是只读的,因为它的本质是输入接口。
注意,import 命令具有提升效果,会提升到整个模块的头部,首先执行,同时 .js 后缀可以省略。
由于 import 是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

export default

为了给用户提供方便,让他们不用阅读文档就能加载模块(不需要知道名字),就要用到 export default 命令,为模块指定默认输出。
其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

1
2
3
4
5
6
7
8
export default function () {
console.log('foo');
}

import customName from './export-default';

// 导入默认与非默认方法
import _, { each, forEach } from 'lodash';

这时 import 命令后面,不使用大括号。显然,一个模块只能有一个默认输出,因此 export default 命令只能使用一次。所以,import 命令后面才不用加大括号,因为只可能唯一对应 export default命令。
本质上,export default 就是输出一个叫做 default 的变量或方法,然后系统允许你为它取任意名字。正是如此所以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 正确
export var a = 1;

// 正确
var a = 1;
export default a;

// 错误
export default var a = 1;

// 正确
export default 42;

// 报错
export 42;

静态化固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。
如果 import 命令要取代 Node 的 require 方法,这就形成了一个障碍。因为 require 是运行时加载模块,import 命令无法取代 require 的动态加载功能

1
2
const path = './' + fileName;
const myModual = require(path);

因此,有一个提案,建议引入 import() 函数,完成动态加载,对于这个import 函数,我就不多进行了解了。

其他

关于 a 标签默认行为(href 跳转):
常见的阻止默认行为的方式:<a href="javascript:void(0);" onclick= "myjs( )"> Click Me </a>
函数 onclick 要优于 href 执行,而 void 是一个操作符,void(0) 返回 undefined,地址不发生跳转,使用 javascript:; 也是一样的效果。
在 onclick 函数中,如果返回的是 true,则认为该链接发生了点击行为;如果返回为 false,则认为未被点击。

参考

http://es6.ruanyifeng.com/#docs/class

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~