ECMAScript 6(以下简称ES6)是JavaScript语言的下一代标准。以下总结内容都是es6中新的特性,也是开发中经常用到知识点。
变量声明const和let
在ES6之前,我们都是用var声明变量,无论声明在何处,都会有变量提升(被视为声明在函数的最顶部,不在函数内即在全局作用域的最顶部)。
而es6之后,新增了let和const,声明不会出现没有变量提升,且都不允许重复声明。const声明一个只读的常量。一旦声明,常量的值就不能改变。一旦声明就必须立即初始化。
let和const都是块级作用域,看下面经典例子
var a=[];
//执行for循环
for(var i=0;i<10;i++){
a[i]=function(){ //因为这个是定义,并没有调用方法,不会执行
console.log(i);
};
}
//for循环之后,此时 i = 10;
再次执行a6;因为 i 一直被引用,所以不会回收,
进入到 a[i] 的方法里面, 打印的是 i ,也就是10a6; //输出10
如果使用let声明i
var a=[];
for(let i=0;i<10;i++){
a[i]=function(){
console.log(i);
};
}
a[6](); //打印6
a[6]函数(闭包)这个执行环境中,它会首先寻找该执行环境中是否存在 i,没有找到,因为 i 是块级作用域,就沿着作用域链继续向上到了其所在的代码块执行环境,找到了i=6,于是输出了6,即a6;的结果为6。这时,闭包被调用,所以整个代码块中的变量i和函数a6被销毁。
模板字符串
传统的 JavaScript 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' + '<em>' + basket.onSale +
'</em> are on sale!'
);
上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。
$('#result').append(
` There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em> are on sale!`);
对于ES6当然也提供了很多字符串的方法。
// 1.includes:判断是否包含然后直接返回布尔值
const str = 'hahay'
console.log(str.includes('y')) // true
// 2.repeat: 获取字符串重复n次
const str = 'he'
console.log(str.repeat(3)) // 'hehehe'
//如果你带入小数, Math.floor(num) 来处理
// s.repeat(3.1) 或者 s.repeat(3.9) 都当做成 s.repeat(3) 来处理
// 3. startsWith 和 endsWith 判断是否以 给定文本 开始或者结束
const str = 'hello world!'
console.log(str.startsWith('hello')) // true
console.log(str.endsWith('!')) // true
解构赋值
以前我们给变量赋值,只能直接指定值
var a = 1;var b = 2;var c = 3;console.log(a,b,c); // 1 2 3
现在用解构赋值的写法就变得简单了,只要模式匹配上了就行了,如下
注意数组是有顺序的
var [a,b,c] = [11,22,33];
console.log(a,b,c); // 11 22 33
var [b,a,c] = [11,22,33];
console.log(a,b,c); // 22 11 33
当然解构赋值还有嵌套比较复杂的写法,如下
let [foo,[[bar],[baz]]] = [111,[[222],[333]]];
console.log(foo,bar,baz); // 111 222 333
let [head,…foot] = [1,2,3,4];
console.log(head,foot); // 1 [2,3,4]
如果解构不成功,变量的值就等于undefined,如下
var [bar3,foo3] = [1000];
console.log(bar3,foo3); // 1000 undefined
另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功
let [x,y] = [10000,20000,30000];
console.log(x,y); // 10000 20000
默认值可以引用解构赋值的其他变量,但该变量必须已经声明
let [a=1,b=a] = [2,3];
console.log(a,b); // 2 3
对象的解构也可以指定默认值
var {x,y=5} = {x:1};
console.log(x,y); // 1 5
对象的解构赋值解构不仅可以用于数组,还可以用于对象(json)
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;
而对象的属性没有次序,变量必须与属性同名,才能取到正确的值
var {a,b} = {a:'apple',b:'banana'};
console.log(a,b); // apple banana
var {b,a} = {a:'apple',b:'banana'};
console.log(a,b); // apple banana
如果变量名与属性名不一致,必须写成下面这样
let obj = {first:'hello',last:'world'};
// first ---> f,那么此时f就是first,而不是undefined了,有点类似别名的概念
let {first:f,last} = obj;
console.log(f,last); // hello world
1.也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。 真正被赋值的是后者,而不是前者
2.v是匹配的模式,n才是变量。真正被赋值的是变量n,而不是模式v。
//注意,采用这种写法时,变量的声明和赋值是一体的
v —> n,那么此时n就是vue,而不是undefined了
var {v:n} = {v:'vue',r:'react'};
console.log(n); // vue
console.log(v); // Uncaught ReferenceError: v is not defined
console.log(r); // Uncaught ReferenceError: r is not defined
函数
函数默认参数
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
function log(x, y) {
if(typeof y ==="undefined") {
y="world"
} console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World
上面代码检查函数log的参数y有没有赋值,如果没有,则指定默认值为World
ES6为参数提供了默认值。在定义函数时便初始化了这个参数,以便在参数没有被传递进去时使用。
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
箭头函数
ES6 允许使用“箭头”(=>)定义函数。
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
箭头函数最直观的三个特点:
不需要 function 关键字来创建函数
省略 return 关键字
继承当前上下文的 this 关键字,this固定化
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2)
{ return num1 + num2;}
所谓箭头函数的 this 捕获的是所在的上下文,比如下面这个例子:b是一个箭头函数,然后它的 this是指向window,这是为什么呢,因为箭头函数捕获的是obj{}这个对象的环境,然后这个环境的this指向的是window,就相当于上一条的例子:在c方法里面return的那个箭头函数捕获的是c:function(){}这个环境的this,而这个环境的this是obj
var obj = {
a: 10,
b: () => { console.log(this.a); //undefined
console.log(this); //window
},
c: function() { console.log(this.a); //10
console.log(this); //obj{...}
}
}
obj.b();
obj.c();
对于函数的this指向问题:
1、箭头函数的this永远指向其上下文的 this,任何方法都改变不了其指向,如call(), bind(), apply()
2、普通函数的this指向调用它的那个对象
对象的扩展
对象初始化简写
ES6 允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo};
function f(x, y){
return {x, y};
}
// 等同于
function f(x, y) { return {x: x, y: y};
}
除了属性简写,方法也可以简写
const o = {
method() {
return "Hello!";
}
};
// 等同于const o = {
method: function() {
return "Hello!";
}
};
ES6 对象提供了 Object.assign() 这个方法来实现浅复制。
Object.assign() 可以把任意多个源对象自身可枚举的属性拷贝给目标对象,然后返回目标对象。第一参数即为目标对象。在实际项目中,我们为了不改变源对象。一般会把目标对象传为{}。
const objA = { name: 'cc', age: 18 }
const objB = { address: 'beijing' }
const objC = {} // 这个为目标对象
const obj = Object.assign(objC, objA, objB)
// 我们将 objA objB objC obj 分别输出看看
console.log(objA) // { name: 'cc', age: 18 }
console.log(objB) // { address: 'beijing' }
console.log(objC) // { name: 'cc', age: 18, address: 'beijing' }
console.log(obj) // { name: 'cc', age: 18, address: 'beijing' }
// 是的,目标对象ObjC的值被改变了。
// so,如果objC也是你的一个源对象的话。请在objC前面填在一个目标对象{}
Object.assign({}, objC, objA, objB)
rest参数
ES6 引入 rest 参数(形式为…变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
//利用rest参数, 可以向该函数传入任意数目的参数
function add(...values) {
let sum = 0; for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
//rest参数中的变量代表一个数组, 所以数组特有的方法都可以用于这个变量
function push(array, ...items) {
items.forEach(function(item) {
array.push(item); console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
注:rest参数之后不能再有其他参数( 即只能是最后一个参数)
展开运算符
扩展运算符( spread) 是三个点( …) 。 它好比rest参数的逆运算, 将一个数组转为用逗号分隔的参数序列。
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, ...more]
[...arr1, ...arr2, ...arr3]
//转为真正的数组。
var nodeList = document.querySelectorAll('div');
var array = [...nodeList];
//map转数组
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
对于 Object 而言,还可以用于组合成新的 Object 。(ES2017 stage-2 proposal) 当然如果有重复的属性名,右边覆盖左边。
const first = {
a: 1,
b: 2,
c: 6,
}
const second = {
c: 3,
d: 4
}
const total = { ...first, ...second }
console.log(total) // { a: 1, b: 2, c: 3, d: 4 }
import 和 export
import导入模块、export导出模块:
//全部导入import people from './example'
//有一种特殊情况,即允许你将整个模块当作单一对象进行导入
//该模块的所有导出都会作为对象的属性存在
import * as example from "./example.js"
console.log(example.name)
console.log(example.age)
console.log(example.getName())
//导入部分
import {name, age} from './example'
// 导出默认, 有且只有一个默认
export default App
// 部分导出
export class App extend Component {};
导入的时候有没有大括号的区别是什么。下面是总结:
当用export default people导出时,就用 import people 导入(不带大括号)。
一个文件里,有且只能有一个export default。但可以有多个export。
当用export name 时,就用 import{name} 导入(记得带上大括号)。
当一个文件里,既有一个export default people, 又有多个export name 或者 export age时,导入就用 importpeople,{name,age}。
当一个文件里出现n多个 export 导出很多模块,导入时除了一个一个导入,也可以用 import*asexample。
- Promise
Promise 是异步编程的一种解决方案,比传统的解决方案–回调函数和事件,
在promise之前代码过多的回调或者嵌套,可读性差、耦合度高、扩展性低。通过Promise,用同步编程的方式来编写异步代码,极大的降低了代码耦合性而提高了程序的可扩展性。
下面是一个用Promise对象实现的 Ajax 操作的例子。
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json")
.then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
Promise
在promise之前代码过多的回调或者嵌套,可读性差、耦合度高、扩展性低。通过Promise机制,扁平化的代码机构,大大提高了代码可读性;用同步编程的方式来编写异步代码,保存线性的代码逻辑,极大的降低了代码耦合性而提高了程序的可扩展性。
就是用同步的方式去写异步代码。
//Promise对象 ---> 用来传递异步操作过来的数据的
//Pending(等待、处理中) ---> Resolve(完成,fullFilled) ---> Reject(拒绝,失败)
//这里只是定义,还没开始执行
var p1 = new Promise(function(resolve,reject){
resolve(1); // 成功了,返回一个promise对象1
// reject(2); // 失败了
});
// 接收成功和失败的数据,通过then来传递
// then也是返回一个promise对象,会继续往下传递数据,传递给下一个then
p1.then(function(value){
// resolve
console.log(value); //执行打印1
return value + 1; // 1
alert(`成功了:${value}`);
},function(value){
// reject
alert(`失败了:${value}`);
}).then(function(value){
console.log(value); // 2
});
//catch捕获异常错误
var p1 = new Promise(function(resolve,reject){
resolve('成功了'); //返回一个promise对象“成功了”
});
//then也是返回一个promise对象,会继续往下传递数据
p1.then(function(value){
console.log(value); //打印“成功了”
// throw是用来抛错误的
throw '发生了点小意外';
}).catch(function(e){
// catch用来捕获这个错误的 ---> 追踪
console.log(e);
});
//all ---> 全部,用于将多个promise对象,组合,包装成
//Promise.all([p1,p2,p3,...]); 所有的promise对象,都正确,才走成功
//否则,只要有一个错误,就走失败
var p1 = Promise.resolve(1);
var p2 = Promise.reject(0);
Promise.all([true,p1,p2]).then(function(obj){
console.log(`成功了:${obj}`);
},function(obj){
console.log(`失败了:${obj}`);
});
// race ---> 返回的也是一个promise对象
//最先执行的的promise结果,哪个最快我用哪个,所以下面打印的是one
var p1 = new Promise(function(resolve,reject){
setTimeout(resolve,50,'one');
});
var p2 = new Promise(function(resolve,reject){
setTimeout(resolve,100,'two');
});
Promise.race([p1,p2]).then(function(val){
console.log(val);
});
//resolve ---> 生成一个成功的promise对象
//语法规则:Promise.resolve(val); // 普通值
// Promise.resolve(arr); // 数组之类
//Promise.resolve(promise); // 传递另一个promise对象
//传递普通值
Promise.resolve('success').then(function(val){
// 注意resolve,走得是这里
console.log(val); // success
},function(err){
console.log("err:"+ err);
});
//传递数组
Promise.resolve([1,2,3]).then(function(val){
// 注意resolve,走得是这里
console.log(val); // [1,2,3]
},function(err){
console.log(err);
});
//传递一个promise对象
var p1 = Promise.resolve(520);
var p2 = Promise.resolve(p1);
p2.then(function(val){
//从p1那边传递过来的
console.log(val); // 520
});
再来一道经典面试题:
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function executor(resolve) {
console.log(2);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5);
首先先碰到一个 setTimeout,于是会先设置一个定时,在定时结束后将传递这个函数放到任务队列里面,因此开始肯定不会输出 1 。
然后是一个 Promise,里面的函数是直接执行的,因此应该直接输出 2 3 。
然后,Promise 的 then 应当会放到当前 tick 的最后,但是还是在当前 tick 中。
因此,应当先输出 5,然后再输出 4 。
最后在到下一个 tick,就是 1 。
“2 3 5 4 1”
Generators
在异步编程中,还有一种常用的解决方案,它就是Generator生成器函数。顾名思义,它是一个生成器,它也是一个状态机,内部拥有值及相关的状态,生成器返回一个迭代器Iterator对象,我们可以通过这个迭代器,手动地遍历相关的值、状态,保证正确的执行顺序。
当你调用一个generator时,它将返回一个迭代器对象。这个迭代器对象拥有一个叫做next的方法来帮助你重启generator函数并得到下一个值。next方法不仅返回值,它返回的对象具有两个属性:done和value。value是你获得的值,done用来表明你的generator是否已经停止提供值。
Generator的声明方式类似一般的函数声明,只是多了个*号,并且一般可以在函数内看到yield关键字
function* showWords() {
yield 'one';
yield 'two';
return 'three';
}
var show = showWords();
show.next() // {done: false, value: "one"}
show.next() // {done: false, value: "two"}
show.next() // {done: true, value: "three"}
show.next() // {done: true, value: undefined}
如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的
function* foo() {
yield 'a';
yield 'b';
}
function* bar(){
yield 'x';
foo();
yield 'y';
}
for (let v of bar()){
console.log(v);
}// "x"// "y"
这个就需要用到yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}
var it = main();
it.next();
上面代码的main函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield表达式,本身是没有值的,总是等于undefined。
async 函数
es6引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name);
const stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog')
.then(function (result) { console.log(result);});
一比较就会发现,async函数就是将 Generator 函数的星号“*” 替换成async,将yield替换成await,仅此而已。
async函数对 Generator 函数的改进,体现在以下四点:
内置执行器
更好的语义
更广的适用性
返回值是 Promise
Class基本语法
JavaScript 语言中,生成实例对象的传统方法是通过构造函数:
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(类)这个概念,作为对象的模板。通过class关键字,可以定义类。
基本上,es6 的可以看作只是一个语法糖,它的绝大部分功能,es5 都可以做到,新的写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 es6 的%(red)[class]改写,就是下面这样。
//定义类
class Point {
constructor(x, y) {
this.x = x; this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,es5 的构造函数Point,对应 es6 的Point类的构造方法。
Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。