首先集百家之介绍:
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 | { |
例如 for 循环就合适使用 let 定义 i
for
循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
var
命令会发生“变量提升”现象,即变量可以在声明之前使用,值为 undefined
。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
为了纠正这种现象,let
命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
ES6 明确规定,如果区块中存在
let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
ES6 规定,块级作用域之中,函数声明语句的行为类似于let
,在块级作用域之外不可引用。
总之,在代码块内,使用 let
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ),不过应该提倡能用 let 的时候尽量别用 var,避免造成作用域的混乱。
const
声明一个只读的常量。一旦声明,常量的值就不能改变。
这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值。const
的作用域与let
命令相同:只在声明所在的块级作用域内有效,不提升、存在暂时性死区。
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。
字符串
在 ES6 中,对字符串进行了增强,尤其是模板字符串,真是非常的好用!
模板字符串
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量或者调用函数。
1 | // 普通字符串 |
如果模板字符串中的变量没有声明,将报错。如果大括号中的值不是字符串,将按照一般的规则(toString)转为字符串。
新增方法
ES5 字符串的实例方法很有限,基本就是 indexOf 了,在 ES6 新加入了一些:
- includes():返回布尔值,表示是否找到了参数字符串。
- startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
- endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
- repeat():返回一个新字符串,表示将原字符串重复 n 次。
在 ES2017 和 ES2019 又引入了 padStart()
用于头部补全,padEnd()
用于尾部补全和 trimStart()
和 trimEnd()
这两个方法。
函数
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
1 | function log(x, y = 'World') { |
ES6 引入 rest 参数(形式为 ...变量名
),用于获取函数的多余参数,本质是个数组,跟 Java 很类似:
1 | function add(...values) { |
其次还有函数的 name
属性,返回该函数的函数名。
箭头函数
ES6 允许使用“箭头”(=>
)定义函数。
1 | var f = v => v; |
怎么说呢,这个其实就是简化的匿名函数,用在回调的地方非常好用。箭头函数有几个使用注意点。
- 函数体内的
this
对象,就是定义时所在的对象,而不是使用时所在的对象。 - 不可以当作构造函数,也就是说,不可以使用
new
命令,否则会抛出一个错误。 - 不可以使用
arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。 - 不可以使用
yield
命令,因此箭头函数不能用作 Generator 函数。
其中第一点尤其值得注意。this
对象的指向是可变的,但是在箭头函数中,它是固定的。
1 | function foo() { |
关于 this 的这个问题,版本对比为:
1 | // ES6 版本 |
实际原因是箭头函数根本没有自己的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 | function func(a, b, c) { |
call 是把第二个及以后的参数作为 func 方法的实参传进去,而 func1 方法的实参实则是在 bind 中参数的基础上再往后排。
数组的扩展
扩展运算符(spread)是三个点(...
)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
1 | console.log(...[1, 2, 3]) |
对于数组的克隆与合并,有了扩展运算符也变得简单多了:
1 | const a1 = [1, 2]; |
ES5 中只能使用 concat 函数间接达到目的。
字符串也可以被展开:[...'hello']
,还可以用于 Generator 函数:
1 | const go = function*(){ |
对象扩展
现在对象的属性有了更简洁的写法:
1 | const baz = {foo}; |
简单说就是当 key 和 val 一样时,可以进行简写。其实,方法也可以进行简写:
1 | const o = { |
这种写法会非常的简洁,另外常用的还有 setter 和 getter,就是采用的这种方案:
1 | const cart = { |
需要注意的一点就是简洁写法的属性名总是字符串。在对象定义上,也变得更加灵活了:
1 | let propKey = 'foo'; |
ES6 又新增了另一个类似的关键字super
,指向当前对象的原型对象。
另外,对象也有扩展运算符,例如:
1 | let z = { a: 3, b: 4 }; |
简单说就是把对象里的方法进行拷贝,Vuex 中的这种写法算是明白了吧,Vuex 中,我们经常用类似 ...mapState({xxx})
的写法,很显然 mapState 函数返回的是一个对象,然后我们使用“展开运算符”将其展开了。
遍历
变量数组或者对象,可以使用 forEach 这个函数(ES5 中也可使用):
1 | [1, 2 ,3, 4].forEach(alert); |
使用 forEach 函数进行遍历时,中途无法跳过或者退出;
在 forEach 中的 return、break、continue 是无效的。
然后遍历除了基本的 fori,还有两种:for...in
和 for...of
,那么他们俩有啥区别呢?
- 推荐在循环对象属性的时候,使用
for...in
,在遍历数组的时候的时候使用for...of
。 for...in
循环出的是 key,for...of
循环出的是 value- 注意,
for...of
是 ES6 新引入的特性。修复了 ES5 引入的for...in
的不足 for...of
不能循环普通的对象,需要通过和Object.keys()
搭配使用
下面是一段示例代码:
1 | let aArray = ['a',123,{a:'1',b:'2'}] |
作用于数组的 for-in
循环除了遍历数组元素以外,还会遍历自定义属性。for...of
循环不会循环对象的 key,只会循环出数组的 value,因此 for...of
不能循环遍历普通对象,对普通对象的属性遍历推荐使用 for...in
reduce
某次,遇到一个做累加的需求,用传统的方式肯定是没问题,但是我想到既然是动态语言,就没有什么骚操作?
结果搜了一下,确实有很多骚操作,还有直接用 eval 黑魔法的,不过,我觉得比较优雅的就是 reduce 方法了:
1 | var arr = [1,2,3] |
总感觉似曾相识,不知道在哪里用过,也许是 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 | // 将数组中的数据翻倍 |
如果你习惯使用函数是编程,那么肯定喜欢使用 map()
。因为 forEach()
会改变原始的数组的值,而 map()
会返回一个全新的数组,原本的数组不受到影响。
总之,能用forEach()
做到的,map()
同样可以。反过来也是如此。
一般来说,使用 map 速度会更快,测试地址:https://jsperf.com/map-vs-foreach-speed-test
Class语法
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过 class
关键字,可以定义类。
基本上,ES6 的 class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
1 | // ES5 |
对于做静态语言后端的我,果然还是 ES6 的写法更舒服。
定义“类”的方法的时候,前面不需要加上
function
这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。
既然说 class 只是一个语法糖,那么我们就要深入一点看看了:
1 | class Point { |
类的数据类型就是函数,类本身就指向构造函数。
构造函数的 prototype
属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的 prototype
属性上面。
类的内部所有定义的方法,都是不可枚举的(non-enumerable),这一点与 ES5 的行为不一致。
生成实例对象如果忘记加上 new
,像函数那样调用 Class
,将会报错。
类不存在变量提升(hoist),也就是没办法先使用后定义。
此外还有很多需要注意的点,不过我认为我知道这一部分就足够了,了解更多就去看阮一峰的书吧。
Promise函数
在 JavaScript 的世界中,所有代码都是单线程执行的。由于这个“缺陷”,导致 JavaScript 的所有网络操作,浏览器事件,都必须是异步执行,也就是要通过异步来处理。
Promise 有各种开源实现,在 ES6 中被统一规范,由浏览器直接支持。
1 | function test(resolve, reject) { |
就我来说,它最重要的功能是来解决回调地狱问题,解决异步中回调的多层嵌套。
还有一个,就是异步剥夺了 return 的权利,你用异步,return 基本就没啥意义,只能通过传入方法来执行,也是相当于回调了。
async函数
异步操作是 JavaScript 编程的麻烦事,麻烦到一直有人提出各种各样的方案,试图解决这个问题。从最早的回调函数,到 Promise 对象,再到 Generator 函数,每次都有所改进,但又让人觉得不彻底,异步编程的最高境界,就是根本不用关心它是不是异步。
一句话,async 函数就是 Generator 函数的语法糖。
1 | var fs = require('fs'); |
async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。
async 自带执行器,相比 Generator 也有更好的寓意,async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中。
await 命令只能用在 async 函数之中,如果用在普通函数,就会报错。
Module语法
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的
require
、Python 的import
,甚至就连 CSS 都有@import
,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
1 | // CommonJS 模块 |
上面代码的实质是整体加载 fs
模块(即加载 fs
的所有方法),生成一个对象(_fs
),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过 export
命令显式指定输出的代码,再通过 import
命令输入。
1 | // ES6模块 |
这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
export
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用 export
关键字输出该变量。
下面展示一下几种 export 的写法:
1 | // 第一种 |
它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系,所以你不能直接输出一个值,例如数字。
1 | // 报错 |
export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。
import
使用export
命令定义了模块的对外接口以后,其他 JS 文件就可以通过import
命令加载这个模块。
1 | // 自定义函数名 |
import
命令输入的变量都是只读的,因为它的本质是输入接口。
注意,import
命令具有提升效果,会提升到整个模块的头部,首先执行,同时 .js
后缀可以省略。
由于 import
是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
export default
为了给用户提供方便,让他们不用阅读文档就能加载模块(不需要知道名字),就要用到 export default
命令,为模块指定默认输出。
其他模块加载该模块时,import
命令可以为该匿名函数指定任意名字。
1 | export default function () { |
这时 import
命令后面,不使用大括号。显然,一个模块只能有一个默认输出,因此 export default
命令只能使用一次。所以,import 命令后面才不用加大括号,因为只可能唯一对应 export default
命令。
本质上,export default
就是输出一个叫做 default
的变量或方法,然后系统允许你为它取任意名字。正是如此所以:
1 | // 正确 |
静态化固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。
如果 import
命令要取代 Node 的 require
方法,这就形成了一个障碍。因为 require
是运行时加载模块,import
命令无法取代 require
的动态加载功能。
1 | const path = './' + fileName; |
因此,有一个提案,建议引入 import()
函数,完成动态加载,对于这个import 函数,我就不多进行了解了。
其他
关于 a 标签默认行为(href 跳转):
常见的阻止默认行为的方式:<a href="javascript:void(0);" onclick= "myjs( )"> Click Me </a>
函数 onclick 要优于 href 执行,而 void 是一个操作符,void(0)
返回 undefined,地址不发生跳转,使用 javascript:;
也是一样的效果。
在 onclick 函数中,如果返回的是 true,则认为该链接发生了点击行为;如果返回为 false,则认为未被点击。
评论框加载失败,无法访问 Disqus
你可能需要魔法上网~~