一、ES5 ES6区别
1、let和var定义变量的区别
javascript 严格模式
第一次接触let关键字,有一个要非常非常要注意的概念就是”JavaScript 严格模式”,比如下述的代码运行就会报错:
let hello = 'hello world.'; console.log(hello);
错误信息如下:
let hello = 'hello world.';
^^^
SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode
...
解决方法就是,在文件头添加”javascript 严格模式”声明:
'use strict'; let hello = 'hello world.'; console.log(hello);
let和var关键字的异同
声明后未赋值,表现相同
'use strict'; (function() { var varTest; let letTest; console.log(varTest); //输出undefined console.log(letTest); //输出undefined }());
使用未声明的变量,表现不同:
(function() { console.log(varTest); //输出undefined(注意要注释掉下面一行才能运行) console.log(letTest); //直接报错:ReferenceError: letTest is not defined var varTest = 'test var OK.'; let letTest = 'test let OK.'; }());
重复声明同一个变量时,表现不同:
'use strict'; (function() { var varTest = 'test var OK.'; let letTest = 'test let OK.'; var varTest = 'varTest changed.'; let letTest = 'letTest changed.'; //直接报错:SyntaxError: Identifier 'letTest' has already been declared console.log(varTest); //输出varTest changed.(注意要注释掉上面letTest变量的重复声明才能运行) console.log(letTest); }());
变量作用范围,表现不同
'use strict'; (function() { var varTest = 'test var OK.'; let letTest = 'test let OK.'; { var varTest = 'varTest changed.'; let letTest = 'letTest changed.'; } console.log(varTest); //输出"varTest changed.",内部"{}"中声明的varTest变量覆盖外部的letTest声明 console.log(letTest); //输出"test let OK.",内部"{}"中声明的letTest和外部的letTest不是同一个变量 }());
箭头函数 ( => ) 和普通函数 ( function ) 区别
在箭头函数出现之前,每个新定义的函数都有它自己的this值。箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this,总是指向函数定义生效时所在的对象。
// 1. `Person()`构造函数定义 this 作为它自己实例,而`add()`函数定义 this 作为全局对象。 function Person() { this.age = 10; foo: setInterval(function add() { this.age ++; console.log(this.age); }, 100); } new Person().foo; // 2. 通过将this值分配给封闭的变量,可以解决this问题。 (通过bind绑定作用域亦可) function Person() { let _this = this; _this.age = 10; foo: setInterval(function add() { _this.age ++; console.log(_this.age); }, 100); } new Person().foo; // 3. this正确指向person对象 function Person() { this.age = 10; foo: setInterval(() => { this.age ++; console.log(this.age); }, 1000); } new Person().foo;
不能作为构造函数
let Person = () => {}; let person = new Person(); console.log(person); // TypeError: Foo is not a constructor
不绑定arguments
// 箭头函数内部的变量arguments,其实是函数foo的arguments变量。 function foo() { setTimeout(() => { console.log(arguments); }, 100); } foo(1, 2, 3) // ['1': 1, '2': 2, '3': 3] // function foo() { let f = (...args) => args[0]; return f(2); } foo(1); // 2
没有prototype属性
let Person = () => {}; console.log(Person.prototype); // undefined
Babel
Babel是一个广泛使用的ES6转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。可在Babel官网 (http://babeljs.io/) 查看。
使用Gulp 和 Babel 将 ES6 代码转换成 ES5 代码具体过程如下:
安装依赖
- 安装全局 Gulp
npm install -g gulp
- 安装项目中使用的 Gulp
npm install --save-dev gulp
- 安装 Gulp 上 Babel 的插件
npm install --save-dev gulp-babel
- 安装 Babel 上将 ES6 转换成 ES5 的插件
npm install --save-dev babel-preset-es2015
Gulp 配置
- gulpfile.js 的内容
var gulp = require("gulp"); var babel = require("gulp-babel"); gulp.task("default", function () { return gulp.src("src/**/*.js") // ES6 源码存放的路径 .pipe(babel()) .pipe(gulp.dest("dist")); //转换成 ES5 存放的路径 });
- 如果要生成 Soucemap, 则用 gulp-sourcemaps
var gulp = require("gulp"); var sourcemaps = require("gulp-sourcemaps"); var babel = require("gulp-babel"); var concat = require("gulp-concat"); gulp.task("default", function () { return gulp.src("src/**/*.js") .pipe(sourcemaps.init()) .pipe(babel()) .pipe(concat("all.js")) .pipe(sourcemaps.write(".")) .pipe(gulp.dest("dist")); });
Babel 配置
在项目根路径创建文件 .babelrc。内容为:
{ "presets": ["es2015"] }
执行转换
命令行中执行
gulp
Set 和 Map 数据结构
与 Array 增、删、改、查对比
let map = new Map(); let set = new Set(); let array = []; // 增 map.set('t', 1); set.add( { t : 1 } ); array.push( { t:1 } ); console.info( map, set, array ); // Map { 't' => 1 } Set { { t: 1 } } [ { t: 1 } ] // 查 let map_exist = map.has( 't' ); let set_exist = set.has( {t:1} ); let array_exist = array.find(item => item.t) console.info(map_exist, set_exist, array_exist); //true false { t: 1 } // 改 map.set('t', 2); set.forEach(item => item.t ? item.t = 2:''); array.forEach(item => item.t ? item.t = 2:''); console.info(map, set, array); // Map { 't' => 2 } Set { { t: 2 } } [ { t: 2 } ] // 删 map.delete('t'); set.forEach(item => item.t ? set.delete(item):''); let index = array.findIndex(item => item.t); array.splice(index,1); console.info(map, set, array); // Map {} Set {} []
class、extends、super
ES5中原型、构造函数,继承问题一直困扰我们。ES6引入了Class(类)的概念。新的class写法让对象原型的写法更加清晰、更像面向对象编程的语法,也更加通俗易懂。
class Human { constructor(name) { this.name = name; } sleep() { console.log(this.name + " is sleeping"); } } let man = new Human("Davis"); man.sleep(); //Davis is sleeping class Boy extends Human { constructor(name, age) { super() this.name = name; this.age = age; } info(){ console.log(this.name + 'is ' + this.age + 'years old'); } } let son = new Boy('Faker','8'); son.sleep(); // Faker is sleeping son.info(); // Faker is 8 years old
上面代码首先用class定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。简单地说,constructor内定义的方法和属性是实例对象自己的,而constructor外定义的方法和属性则是所有实例对象可以共享的。
Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。上面定义了一个Boy类,该类通过extends关键字,继承了Human类的所有属性和方法。
super关键字,它指代父类的实例(即父类的this对象)。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
ES6的继承机制,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
template string 模板字符串
ES6中允许使用反引号 来创建字符串,此种方法创建的字符串里面可以包含由美元符号加花括号包裹的变量${vraible}。 ```javascript let num = Math.random(); console.log(
num is ${num}); // num is xx ``` 不再通过 \ 来做多行字符串拼接,模板字符串可以多行书写: ```html $("#main").html(
今天天气很好!
产生一个随机数${num}
`);
```
模板字符串中所有的空格、新行、缩进,都会原样输出在生成的字符串中。
destructuring 解构
ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。
用途一,交换变量的值,不再需要中间变量
let a = 1; let b = 2; [a, b] = [b, a]; console.log(a, b); // 2 1
用途二,提取JSON数据
let jsonData = { id: 1, title: "OK", data: [5, 6] }; let {id, title, data:number} = jsonData; console.log(id, title, number); // 1, "OK", [5, 6]
用途三,函数参数的定义
// 参数是一组有次序的值 function f([x, y, z]) { ... } f([1, 2, 3]); // 参数是一组无次序的值 function f({x, y, z}) { ... } f({z: 3, y: 2, x: 1});
default默认参数
default很简单,就是默认值。现在可以在定义函数的时候指定参数的默认值,而不用像以前那样通过逻辑或操作符来达到目的了。
// 传统指定默认参数 function say1(name) { var name = name || 'Faker'; console.log( 'Hello ' + name ); } // ES6默认参数 function say2(name='Davis') { console.log(`Hello ${name}`); } say1(); // Hello Faker say1('Tom'); // Hello tom say2(); //Hello Davis say2('Bob'); // Hello Bob
注意: say2(name='tom')这里的等号,指的是没有传这个参数,则设置默认值Davis,而不是给参数赋值。
rest参数
rest参数只包括那些没有给出名称的参数;
rest参数是Array的实例,可以直接应用sort, map, forEach, pop等方法;
rest参数之后不能再有其它参数(即,只能是最后一个参数);
函数的length属性,不包括rest参数;
function fn(x, y, ...rest){ console.log(rest) } fn(1, "cat", "dog", 2); //["dog", 2] console.log(fn.length); //2
Proxy 代理
Proxy可以监听对象身上发生了什么事情,并在这些事情发生后执行一些相应的操作。让我们对一个对象有了很强的追踪能力,同时在数据绑定方面也很有用处。
//定义被监听的目标对象 let man = { name: 'Davis', age: 21 }; //定义处理程序 let handle = { set(receiver, property, value) { console.log(property, 'is changed to', value); receiver[property] = value; } }; //创建代理以进行侦听 man = new Proxy(man, handle); //做一些改动来触发代理 man.age = 22; //age is change to 22 man.name = "Faker"; // name is change to Faker
Promise
Promise对象状态
Promise/A+规范, 规定Promise对象是一个有限状态机。它三个状态:
pending(执行中)
Resolved(已完成)
Rejected(已失败)
其中pending为初始状态,Resolved和rejected为结束状态(表示promise的生命周期已结束)。
let val = 1; // 我们假设step1, step2, step3都是ajax调用后端或者是在Node.js上查询数据库的异步操作 // 每个步骤都有对应的失败和成功处理回调 // step1、step2、step3必须按顺序执行 function step1(resolve, reject) { console.log('步骤一:执行'); if (val >= 1) { resolve('Hello I am No.1'); } else if (val === 0) { reject(val); } } function step2(resolve, reject) { console.log('步骤二:执行'); if (val === 1) { resolve('Hello I am No.2'); } else if (val === 0) { reject(val); } } function step3(resolve, reject) { console.log('步骤三:执行'); if (val === 1) { resolve('Hello I am No.3'); } else if (val === 0) { reject(val); } } new Promise(step1).then(function(val){ console.info(val); return new Promise(step2); }).then(function(val){ console.info(val); return new Promise(step3); }).then(function(val){ console.info(val); return val; }).then(function(val){ console.info(val); return val; }); // 执行之后将会打印 步骤一:执行 Hello I am No.1 步骤二:执行 Hello I am No.2 步骤三:执行 Hello I am No.3 Hello I am No.3
常用关键点:
在Promise定义时,函数已经执行了;Promise构造函数只接受一个参数,即带有异步逻辑的函数。这个函数在 new Promise 时已经执行了。只不过在没有调用 then 之前不会 resolve 或 reject。
在then方法中通常传递两个参数,一个 resolve 函数,一个 reject 函数。reject就是出错的时候运行的函数。resolve 函数必须返回一个值才能把链式调用进行下去。
resolve 返回一个新 Promise
返回一个新Promise之后再调用的then就是新Promise中的逻辑了。resolve 返回一个值
返回一个值会传递到下一个then的resolve方法参数中。
Generator
Generator函数跟普通函数的写法有非常大的区别:
function关键字与函数名之间有一个 *;
函数体内部使用yield语句,定义不同的内部状态;
function* f() { yield 'a'; yield 'b'; yield 'c'; return 'ending'; } let fn = f(); console.log(fn.next()); // { value: 'a', done: false } console.log(fn.next()); // { value: 'b', done: false } console.log(fn.next()); // { value: 'c', done: false } console.log(fn.next()); // { value: 'ending', done: true }
第一次输出fn.next()返回一个简单的对象{value: "a", done: false},'a'就是f函数执行到第一个yield语句之后得到的值,false表示f函数还没有执行完,只是在这暂停。
第二次,返回的就是{value: "b", done: false},说明f函数运行到了第二个yield语句,返回的是该yield语句的返回值'b'。返回之后依然是暂停。
第三次,第四次同理,这样整个f函数就运行完毕了。
异步操作的同步化写法
Generator函数的暂停执行的效果,意味着可以把异步操作写在yield语句里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield语句下面,反正要等到调用next方法时再执行。所以,Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
例子:假设我在测试服务器的某目录建了4个文件,分别是'test.html'、'a.html'、'b.html'、'c.html',后三个文件的文件内容跟文件名相同,现在我编辑'test.html'的代码,想要先ajax-get相对网址'a.html',然后再回调里ajax-get相对网址'b.html',然后在回调里ajax-get相对网址'c.html'。
function req(url) { $.get(url, function(res){ it.next(res); }); } // 生成器函数 function* ajaxs() { console.log(yield req('a.html')); console.log(yield req('b.html')); console.log(yield req('c.html')); } var it = ajaxs(); // 遍历器对象 it.next(); // a.html // b.html // c.html
强调:只有当yield后面跟的函数先执行完,无论执行体里面有多少异步回调,都要等所有回调先执行完,才会执行等号赋值,以及再后面的操作。这也是yield最大的特性。
export、import
export用于对外输出本模块(一个文件可以理解为一个模块)变量的接口;
import用于在一个模块中加载另一个含有export接口的模块。
导出一组对象
导出模块文件app.js:
class Human{ constructor(name) { this.name = name; } sleep() { console.log(this.name + " is sleeping"); } } function walk() { console.log('i am walking'); } function play() { console.log('i am playing'); } export { Human, walk }
模块导出了两个对象:Human类和walk函数,能被其他文件使用。而play函数没有导出,为此模块私有,不能被其他文件使用。
main.js导入app.js模块
import { Human, walk } from 'app.js';
Default导出
使用关键字default,可将对象标注为default对象导出。default关键字在每一个模块中只能使用一次。
... //类,函数等 export default App;
main.js导入app.js模块
import App from 'app.js';
二、代码规范
1、不要使用arguments 对象,应使用 ...args 代替。
示例: // good function foo(...args) { console.log(args.join('')); } // bad function foo() { console.log([].join.call(arguments)); }
一个函数被设计为需要call 和 apply 的时候,不能是箭头函数。
解释:
箭头函数会强制绑定当前环境下的 this。
示例: // good let foo = { bar(x, y) { return x + y; } }; // bad let foo = { bar: function (x, y) { return x + y; } };
用数组展开代替 contact 方法,数组展开对 iterable 有更好的兼容性。
示例:
// good let foo = [...foo, newValue]; let bar = [...bar, ...newValues]; // bad let foo = foo.concat(newValue); let bar = bar.concat(newValues);
使用数组展开语法进行复制,代码可读性较差。推荐使用 Array.from 方法进行复制操作。
示例: // good let otherArr = Array.from(arr); // bad let otherArr = [...arr];
map 和 set 是可遍历对象,能够方便地使用 for of 遍历。不要使用使用普通 Object。
示例: // good let membersAge = new Map([ ['one', 10], ['two', 20], ['three', 30] ]); for (let [key, value] of map) { } // bad let membersAge = { one: 10, two: 20, three: 30 }; for (let key in membersAge) { if (membersAge.hasOwnProperty(key)) { let value = membersAge[key]; } }
使用 Object.assign 设置默认值
Bad: var menuConfig = { title: null, body: "Bar", buttonText: null, cancellable: true }; function createMenu(config) { config.title = config.title || "Foo"; config.body = config.body || "Bar"; config.buttonText = config.buttonText || "Baz"; config.cancellable = config.cancellable === undefined ? config.cancellable : true; } createMenu(menuConfig); Good: var menuConfig = { title: "Order", // User did not include 'body' key buttonText: "Send", cancellable: true }; function createMenu(config) { config = Object.assign( { title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true }, config ); // config now equals: {title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true} // ... } createMenu(menuConfig);
Class替换prototype使用
Bad Array.prototype.diff = function(comparisonArray) { var values = []; var hash = {}; for (var i of comparisonArray) { hash[i] = true; } for (var i of this) { if (!hash[i]) { values.push(i); } } return values; }; Good: class SuperArray extends Array { constructor(...args) { super(...args); } diff(comparisonArray) { var values = []; var hash = {}; for (var i of comparisonArray) { hash[i] = true; } for (var i of this) { if (!hash[i]) { values.push(i); } } return values; } }
函数式编程可读性更好,也更易于测试。
Bad: const programmerOutput = [ { name: "Uncle Bobby", linesOfCode: 500 }, { name: "Suzie Q", linesOfCode: 1500 }, { name: "Jimmy Gosling", linesOfCode: 150 }, { name: "Gracie Hopper", linesOfCode: 1000 } ]; var totalOutput = 0; for (var i = 0; i < programmerOutput.length; i++) { totalOutput += programmerOutput[i].linesOfCode; } Good: const programmerOutput = [ { name: "Uncle Bobby", linesOfCode: 500 }, { name: "Suzie Q", linesOfCode: 1500 }, { name: "Jimmy Gosling", linesOfCode: 150 }, { name: "Gracie Hopper", linesOfCode: 1000 } ]; var totalOutput = programmerOutput .map(programmer => programmer.linesOfCode) .reduce((acc, linesOfCode) => acc + linesOfCode, 0);
Bad: class Airplane { //... getCruisingAltitude() { switch (this.type) { case "777": return getMaxAltitude() - getPassengerCount(); case "Air Force One": return getMaxAltitude(); case "Cesna": return getMaxAltitude() - getFuelExpenditure(); } } } Good: class Airplane { //... } class Boeing777 extends Airplane { //... getCruisingAltitude() { return getMaxAltitude() - getPassengerCount(); } } class AirForceOne extends Airplane { //... getCruisingAltitude() { return getMaxAltitude(); } } class Cesna extends Airplane { //... getCruisingAltitude() { return getMaxAltitude() - getFuelExpenditure(); } }
可以通过闭包方式添加私有属性:
Bad: var Employee = function(name) { this.name = name; }; Employee.prototype.getName = function() { return this.name; }; var employee = new Employee("John Doe"); console.log("Employee name: " + employee.getName()); // Employee name: John Doe delete employee.name; console.log("Employee name: " + employee.getName()); // Employee name: undefined Good: var Employee = (function() { function Employee(name) { this.getName = function() { return name; }; } return Employee; })(); var employee = new Employee("John Doe"); console.log("Employee name: " + employee.getName()); // Employee name: John Doe delete employee.name; console.log("Employee name: " + employee.getName()); // Employee name: John Doe
不应该为了多个理由去更改某个类的代码,这样会把某个类塞入过多的功能。最小化你需要去改变某个类的次数对于保证代码的稳定性至关重要,过多的改变代码会影响代码库中依赖于该类的其他模块。
Bad: class UserSettings { constructor(user) { this.user = user; } changeSettings(settings) { if (this.verifyCredentials(user)) { // ... } } verifyCredentials(user) { // ... } } Good: class UserAuth { constructor(user) { this.user = user; } verifyCredentials() { // ... } } class UserSettings { constructor(user) { this.user = user; this.auth = new UserAuth(user); } changeSettings(settings) { if (this.auth.verifyCredentials()) { // ... } } }