es6初步深入学习
es6前言(只作了解就好)
ECMAScript和JavaScript的关系
ECMAScript和JavaScript的关系是,前者是后者的规格,后者是前者的一种实现(另外的ECMAScript方言还有Jscript和ActionScript)。
检测node环境对ES6的支持情况
阮一峰写了一个ES-Checker模块,用来检查各种运行环境对ES6的支持情况。运行下面的命令,可以查看你正在使用的Node环境对ES6的支持程度。
sudo npm i -g es-checker
es-checker
babel转码器
ES6转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。es6在线编辑器/在线转码器。
babel stage
在es6标准出来之前,大家都会参与讨论,把各自觉得好的语法加进去,便有了很多阶段(准标准、草案、提案..)。stage-0是一个提案,里面基本包括大家讨论的所有内容,stage-1 -- stage-3以此类推,stage-3基本确定要进入es6了。所以使用webpack的同学会看到,安装的依赖基本都是babel-preset-stage-0。
babel-polyfill
本人之前开发中遇到过一个问题,array的includes方法(string也有),当时运行在chrome47环境下,整个页面打不开,报错(babel includes can not a function),在chrome52环境下支持。解决办法--安装babel-polyfill。最后在你的文件中import 'babel-polyfill'。
Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。举例来说,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。
let和const
let用于声明变量,const用于声明常量。但是聊这个之前,我们还是简单提提老生常谈的问题吧---scope。
块级作用域
在es5中是不存在块级作用域的,一般我们听得比较多的是函数作用域(定义在函数中的参数和变量在函数外部是不可见的)。
function getVal(boo) {
if (boo) {
var val = 'red'
// ...
return val
} else {
// 这里可以访问 val
return null
}
// 这里也可以访问 val
}
那么在es5中如何使用块级作用域呢?js中在一个函数中定义的变量,当这个函数调用完后,变量会被销毁,我们可以利用这种特性(闭包,the most important feature!!)。
function caniuse() {
for(var i=0;i<10;i++){}
console.log(i); //10
}
caniuse();
function caniuse2() {
(function() {
for(var i=0;i<10;i++){}
})()
console.log(i) //i is not defined
}
caniuse2()
在es6中,任何一对花括号中(for,if)的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
function getVal(boo) {
if (boo) {
let val = 'red'
// ...
return val
} else {
// 这里访问不到 val
return null
}
// 这里也访问不到 val
}
有了es6,上面闭包的写法我们用es6可以不需要用到立即执行函数。
for(let i = 0; i < 10; i++){}
let
let声明的变量只在他的代码块内有效
{
let i = 9;
console.log(i) //9
}
console.log(i) //ReferenceError: i is not defined
function test1() {
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i) //10次10
}, 1000)
}
}
function test2() {
for(let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i) //0-9
}, 1000)
}
}
function test3() {
for(var i = 0; i < 10; i++) {
(function(i) {
setTimeout(function() {
console.log(i) //0-9
}, 100)
})(i)
}
}
test1()
test2()
test3()
不存在变量提升
脚本开始运行时,变量i已经存在了,但是没有值,所以会输出undefined。变量ii用let命令声明,不会发生变量提升。这表示在声明它之前,变量ii是不存在的,这时如果用到它,就会抛出一个错误。
console.log(i) //undefined
console.log(ii) //ReferenceError: ii is not defined
var i;
let ii;
还有一些比较不容易发现的暂时性死区
function test1(y = 1, x = y) {
console.log(x, y) // 1 1
}
function test2(x = y, y = 1) {
console.log(x, y) //error
}
test1()
test2()
不允许重复声明
// 报错
function () {
let a = 10;
var a = 1;
}
const
用const声明,常量的值就不能改变。
const一旦声明变量,就必须立即初始化,不能留到以后赋值。
只所在的块级作用域内有效。(见let)
不能变量提升,存在暂时性死区,只能在声明的位置后面使用。(见let)
在一个scope中不可重复declare。
const foo; // SyntaxError: Missing initializer in const declaration
对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。
const foo = {};
foo.prop = 123;
foo.prop
// 123
foo = {}; // TypeError: "foo" is read-only
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错
const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
小结
ES5只有两种声明变量的方法:var命令和function命令。ES6除了添加let和const命令,另外两种声明变量的方法:import命令和class命令。所以,ES6一共有6种声明变量的方法。
Iterator(简单说下,下次再仔细补充)
有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象、Set和Map结构。Iterator是一种机制,为不同的数据结构提供可遍历操作。可遍历结构,我的理解是可以执行for...of。
Iterator作用:
为各种数据结构,提供一个统一的、简便的访问接口
使得数据结构的成员能够按某种次序排列
ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。
Iterator的遍历过程
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++]} :
{done: true};
}
};
}
for...of
let arr = [2,3,4,,7,8]
for(let val of arr) {
console.log(val) //2,3,4,undefined,7,8
}
for(let k in arr) {
console.log(k) //"0" "1" "2" "4" "5",下标都是字符串哦
}
console.log(arr["2"]) //4
let arr2 = {0: 'a', 1: 'b', 3: 'c'}
for(let key of arr2) {
console.log(key, arr2[key]) //arr2[Symbol.iterator] is not a function
}
for(let key in arr2) {
console.log(key, arr2[key]) //"0" "a"
//"1" "b"
//"3" "c"
}
Iterator的使用场合
解构赋值(下一章)
扩展运算符(spread, 方便,使用频率很高。。。spread arguments in function)。
let [a, ...c] = [3, 5, 6, 7, 7, 9]
console.log(a) //3
console.log(c) //[5, 6, 7, 7, 9]
let [e, ...f, g] = [3, 5, 6, 7, 7, 9]
console.log(e) //3
console.log(f) //[5, 6, 7, 7, 9]
console.log(g) //error
var str = 'hello';
[...str] // ['h','e','l','l','o']
let arr = ['b', 'c'];
['a', ...arr, 'd'] // ['a', 'b', 'c', 'd']
yield*
let generator = function* () {
yield 1;
yield* [2, 4];
yield 5
}
let iterator = generator()
console.log(iterator.next()) //{"done": false, "value": 1}
console.log(iterator.next()) //{"done": false, "value": 2}
console.log(iterator.next()) //{"done": false, "value": 4}
console.log(iterator.next()) //{"done": true, "value": 5}
其他(for...of, Array.form(), Map(), Set())
变量的解构赋值(Destructuring)
解构赋值——我的字面理解是先分解结构,再匹配等号两边相对应的结构,如果解构成功给每个相对应地变量赋值,一般情况下如若不成功,就给对应的变量赋值undefined。
模式匹配
只要等号两边的模式相同,解构完全匹配。左边的变量就会被赋予对应的值。
let [a, ...c] = [3, 5, 6, 7, 7, 9]
console.log(a) //3
console.log(c) //[5, 6, 7, 7, 9]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
不完全解构
等号右边的解构匹配左边的,但是还有多余的值。如果右边不具备Iterator接口,解构的过程中会报错。
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
默认值
之前写redux的时候,经常会用到默认值。
export function receiveCustomers({source=null, origin=null, start_at=null, end_at=null, keyword=null}) {
return {
type: types.CUSTOMERS_QUERY_CHANGE,
source,
origin,
start_at,
end_at,
keyword
}
}
null/undefined
var [x = 1] = [undefined];
x // 1
var [x = 1] = [null];
x // null
function
function aaa() {console.log(1)}
let [a = aaa()] = [1] //a=1
变量不能提升
let [x = y, y = 1] = []; // ReferenceError
对象解构
这个灰常常用。一般用法就行,不需要很怪异的写法,可读性差。总结几点注意事项:
如果要将一个已经声明的变量用于解构赋值,必须非常小心。
默认值生效的条件是,对象的属性值严格等于undefined。
// 错误的写法
var x;
{x} = {x: 1};
// SyntaxError: syntax error
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
对象的扩展
属性的简洁表示
这个比较常用,之前写vuex的时候有使用到。用过vuex/redux的同学应该不会陌生
import {fn1, fn2} from 'action'
//action.js里面有两个方法fn1,fn2,最后 export {fn1,fn2}
//等同于export{fn1: fn1, fn2: fn2}
const { dispatch } = this.props
//等同于const dispatch = this.props.dispatch
vuex: {
actions: {
fn1,
fn2
}
}
//等同于
vuex: {
actions: {
fn1: fn1,
fn2: fn2
}
}
function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}
方法的简写
handleText = e => {
this.setState({
inputValue: e.target.value
})
};
//等同于
handleText: function(e) {
...
}
Object.assign
deep clone and shallow clone
JavaScript存储对象都是存地址的,所以浅复制会导致 obj 和 obj1 指向同一块内存地址,大概的示意图如下。而深复制一般都是开辟一块新的内存地址,将原对象的各个属性逐个复制出去。
let obj = {0: "a", 1: "b", 2: "c"}
let deepObj = JSON.parse(JSON.stringify(obj))//is not a really deep clone, but it works.
let shallowObj = obj
obj[3] = 'd'
console.log(deepObj) //{0: "a", 1: "b", 2: "c"}
console.log(shallowObj) //{0: "a", 1: "b", 2: "c", 3: "d"}
console.log(obj) //{0: "a", 1: "b", 2: "c", 3: "d"}
JSON.parse(JSON.stringify(obj))这样的使用方式并不是真正的深拷贝,因为它会丢失一些东西,一些obj的内在property之类的,
比如
obj_test1 = {
a: 1,
b: 2,
get_a() {
return this.a
}
}
这样的一个obj,你用上面的方式会丢掉get_a,如果你定义一些prototype,也会丢失掉,或者如果包含一些dom元素之类的。但是我们多数时候只是用它来复制数据,不关心对象的方法之类的东西,这样的话是够用的。
常见作用
对象拷贝(copy)
Object.assign方法实行的是浅拷贝,而不是深拷贝,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。之前自己的一个疑惑
let obj = {0: "a", 1: "b", 2: "c"}
let objClone = Object.assign({}, obj)
obj[3] = 'd'
console.log(objClone) // {0: "a", 1: "b", 2: "c"} why the same, but not change
Javascript has five primitive data types:
Number
String
Boolean
Undefined
Null
Anything that doesn’t belong to any of these five primitive types is considered an object.
primitive types are passed by value, while objects passed by reference.
a = {a: 1, b: 2, c: 3, d: {aa: 11, bb: 22}}
a1 = Object.assign({}, a, {a: 2})
a1.d.aa = 'i am shllow copy'
第二行是浅拷贝,a拷贝到a1了,并且把property a的值改成2了 (你觉得a.a会变成2么,为什么?)
第三行把a1.d这个object理得aa这个property改了 (你觉得a.d.aa也会改么,为什么?)
合并对象 (merge)
Object.keys && Object.value
ES5引入了Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的key。Object.values返回value。
Class
不得不说,这是我从没仔细看过的一部分,写react的时候都是三板斧套路。。。
特意留意一下super()。ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。es6中先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
class Teachers extends React.Component {
constructor(props, context) {
super(props, context)
}
componentDidMount(){
const { siteId, dispatch } = this.props
if (siteId) {
dispatch(fetchTeachersList(siteId))
}
}
componentWillReceiveProps(nextProps) {
const { siteId, dispatch } = this.props
if (nextProps.siteId !== siteId) {
dispatch(fetchTeachersList(nextProps.siteId))
}
}
}
export default Teachers
搭配Object.assign()
class Point {
constructor(){
// ...
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
不存在变量提升
new Foo(); // ReferenceError
class Foo {}
class表达式
Class表达式,可以写出立即执行的Class。
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('张三');
person.sayName(); // "张三"
this指向和箭头函数
箭头函数很方便,解决了之前es5遗留下来的问题(继承,this作用域)。箭头函数没有它自己的this值,箭头函数内的this值继承自外围作用域。
//es5中, var self = this;这样的代码你肯定常见,es6箭头函数中不用鸟
var obj = {
field: 'hello',
getField: () => {
console.log(this.field)
}
}
obj.getField() // undefined,这时this是window,
var obj = {
field: 'hello',
getField(){
console.log(this.field)
}
}
obj.getField() // hello,
自己常用的拓展
let arr = [1,2,3,4,5,6]
let arr2 = arr.reduce( (init, prev, index) => {init.push(prev*prev); return init}, [] )
console.log(arr2) //[1,4,9,16,25,36]
警告 yield 关键字通常不能在箭头函数中使用(except when permitted within functions further nested within it)。因此,箭头函数不能用作Generator函数。
Module
js历史上没有标准的模块化。在ES6之前,社区制定了一些模块加载方案,最主要的有CommonJS和AMD两种。前者用于服务器,后者用于浏览器。但是这些都只能在运行时起作用,es6的模块化可以在编译时就能确定模块的依赖关系,以及输入和输出的变量。
模块功能主要由两个命令构成:export和import。先抛出一个问题:以下两行代码有什么区别?
import {message, menu} from 'antd'
import message from 'antd/lib/message'
export
对外的接口名与模块内部变量的关系
export对外的接口名与模块内部变量之间,建立了一一对应的关系。当export一个function时,语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export 1; //error
export var m = 1 //right
var m = 1;
export m//error
export {m} //right
import
注意 import命令具有提升效果,会提升到整个模块的头部,首先执行。所以在文件中的任意位置,exort也一样,不过利于可读性,还是import写在文件开头,export写在文件结尾好点。
注意 import命令接受一个对象(用大括号表示),里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。
import {message, menu} from 'antd'
import message from 'antd/lib/message'
所以这之间的区别就是前者中括号里的变量名要与export出的接口名一致,后者message是随意起的名字,可以不与export出来的变量名保持一致,它代表export default出来的接口。export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export deault命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能对应一个方法。本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。
模块的整体加载
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));