本文涵盖了一些ES6新语法可能造成疑惑的地方和一些建议。
箭头函数看起来像是匿名函数表达式 function(){}
的简写,然而它不是。
这个例子应该很容易看出来会有怎样的问题:
function Apple(){} Apple.prototype.check = ()=>{ console.log(this instanceof Apple); }; (new Apple()).check() // false
使用apply、call、bind改变箭头函数的this指向呢?
var i = 0; var xx = ()=>{ console.log(++i, this) }; var yy = function(){ console.log(++i, this) }; xx(); // 1 window xx.apply([]); // 2 window xx.bind([])(); // 3 window yy(); // 4 window yy.apply([]); // 5 [] yy.bind([])(); // 6 []
显然apply、call、bind无法改变箭头函数的this指向,箭头函数的this确定后无法更改。
在这些场景中不要使用箭头函数:
当你需要正常使用this binding时,如函数构造器、prototype
当你需要动态改变this的时候
针对工作报酬和代码量呈反比的程序猿,在需要用到this binding的场景里,可能比较适合的简写形式是在新对象字面量语法里提供的:
var obj = { hello() { // 少写了一个function耶! console.log('world') } };
//1 fetch(xx, oo).then(handleResultAndReturnsAnPromise(result)); //2 fetch(xx, oo).then(handleResultAndReturnsAnPromise); //3 fetch(xx, oo).then((result) => handleResultAndReturnsAnPromise(result)); //4 fetch(xx, oo).then(function(result) { handleResultAndReturnsAnPromise(result) });
1与2、3、4均不等价:1同步调用了handleResultAndReturnsAnPromise;而2~4均会导致handleResultAndReturnsAnPromise在fetch之后完成
2与3/4则是运行时的调用栈有区别,3/4额外创建了一个匿名函数。
3与4除了this binding的区别,4的调用返回值没有进行返回,这样将导致promise链断裂。
1中需要注意的是, then(promise)
里面传一个 Promise 对象是没有什么意义的,它会被当成 then(null)
,在下面推荐的文章中,它被称作“Promise 穿透”
更多的令人混淆的案例,请继续阅读《 谈谈使用 promise 时候的一些反模式 》。
在node的一些版本中,采用Promise并忘记给promise链增加 catch(fn)
或 then(null, fn)
,将导致代码中的异常被吞掉。
这个问题在新的v8中( node 6.6+
,chrome最新版)会导致一个 UnhandledPromiseRejectionWarning
,防止开发遗漏。
node -e 'Promise.reject()' # UnhandledPromiseRejectionWarning: Unhandled promise rejection
Promise接口和jQuery实现的接口不一样, resolve
只接受单参数, then
的回调也只能拿到单参数。
在Promise规范中的单参数链式调用场景下,可以利用解构、 _.spread
、访问自由变量等方式来处理多个过程中得到的值:
new Promise(function(resolve, reject){ let something = 1, otherstuff = 2; resolve({something, otherstuff}); }).then(function({something, otherstuff}){ // handle something and otherstuff });
Promise.all([ Promise.resolve(40), Promise.resolve(36) ]).then( _.spread(function(first, second){ // first: 40, second: 36 }) );
let someMiddleResult; fetch() .then(function(fetchResult){ someMiddleResult = fetchResult; }) .then(otherHandleFn) .then(function(otherHandleFnResult){ // use both someMiddleResult and otherHandleFnResult now })
出现 reject
接口,应该是第一次前端有机会拿异常处理流程做正常流程(比如*)。不要这样做。
由于 reject(new Error(""))
、 throw new Error("")
都能作为 catch
的入口,一些不可预知的错误被抛出的时候,这样的处理方式将会复杂化catch内的代码。不要用异常处理逻辑来做正常处理流程,这个规则保证了代码可读性与可维护性。
throw
和 reject
都可以作为 catch
的入口,它们更加详细的区别如下:
new Promise((resolve, reject) => { setTimeout(function(){ reject(new Error('hello')); }); }).catch(() => console.log('reject')); // reject new Promise((resolve, reject) => { setTimeout(function(){ throw new Error('hello'); }); }).catch(() => console.log('throw')); // Uncaught Error: hello
reject
能够“穿透”回调;而 throw
限于函数作用域,无法“穿透”回调。
建议:
正常流程请选择在 then
的时候 if..else
,不要用 reject
替代
在需要走异常处理流程的时候封装 Error
抛出,可以最大化的化简 catch
回调里面的处理逻辑,类似于 e instanceof MyDesignedError
由于回调函数里的 throw
无法被自动捕获到,如果需要在回调中 reject
当前 promise,那么我们需要用 reject
而不是 throw
在使用 Promise
接口的 polyfill 的场景,应当在 reject
后加一个 return
看起来 let
和 const
的组合就像是一个能完全灭掉 var
的新特性,但对旧代码不能简单的正则替换掉 var
,因为我们太习惯于滥用它的特性了——主要是声明提升。
一些情形下会造成语法错误:
try { let a = 10; if (a > 2) { throw new Error(); } // ... } catch (err) { console.log(a); // 若为var声明,不报错 // 若为const、let声明:Uncaught ReferenceError: a is not defined }
除了 try..catch
,隐式造就的块级作用域在 for
和 if..else
中也将造成问题:
if(false) { let my = 'bad'; } else { console.log(my); // ReferenceError: my is not defined }
解决方案倒是很简单,将作用域内的 let
放在更靠外层的位置即可。
var
、 let
和 const
的区别如下(部分参考自stackoverflow *
):
作用域: let
和 const
将创造一个块级作用域,在作用域之外此变量不可见,作用域外访问将导致 SyntaxError
; var
遵循函数级作用域
全局影响:全局作用域下的 var
使用等同于设置 window
/ global
之上的内容,但 let
和 const
不会
提升行为: var
声明有提升到当前函数作用域顶部的特性,但 const
和 let
没有,在声明前访问变量将导致 SyntaxError
重新赋值:对 const
变量所做的重新赋值将导致 TypeError
,而 var
和 let
不会
重新声明: var
声明的变量使用 var
再次声明不会出现 SyntaxError
,但 const
、 let
声明的变量不能被重新声明,也不能覆盖掉之前任何形式的声明:
var vVar = 1; const vConst = 2; let vLet = 3; var vVar = 4; // success let vVar = 5; // SyntaxError const vVar = 6; // SyntaxError var vConst = 7; // SyntaxError let vConst = 8; // SyntaxError const vConst = 9; // SyntaxError var vLet = 10; // SyntaxError let vLet = 11; // SyntaxError const vLet = 12; // SyntaxError
本篇章集结 ES6 给予的不同边界条件,部分编译自 You don't know JS
function before(a) { var a = a || 1; console.log(a); } function after(a = 1) { console.log(a); } before(NaN) // 1 after(NaN) // NaN
新的写法的fallback逻辑只针对 undefined
有效。
Object.assign
将赋予所有的可枚举值,但不包含从原型链继承来的值:
let arr = [1, 2, 3], obj = {}; Object.assign(obj, arr); obj[1] // 2 obj.length // undefined Object.getOwnPropertyDescriptors(arr).length.enumerable // false
此外: Object.assign
仅仅进行浅拷贝:
var orig = { a: [1, 2, 3] }, nObj = {}; Object.assign(nObj, orig); orig.a.push(4); nObj.a // [1, 2, 3, 4]
Number.isNaN
和全局空间中的 isNaN
的区别在于不存在隐式转换:
isNaN('number') // true Number.isNaN('number') // false
Object.is
除了区分 正负零这个非常小众的边界
,这个接口相对 ===
更大的意义是判断NaN:
Object.is(NaN, NaN); // true NaN === NaN; // false
Object.is(+0, -0); // false +0 === -0; // true
同样的, arr.includes(xx)
比 arr.lastIndexOf(xx) > -1
好的地方也包括对于NaN的处理:
[1, 2, NaN].includes(NaN); // true
isFinite
和 Number.isFinite
的区别也是后者不存在隐式转换:
isFinite("42"); // true Number.isFinite("42"); // false
Number.isInteger
表示一个数是不是小数,和 x === Math.floor(x)
的区别在于对 Infinity
的处理
Number.isInteger(Infinity); // false Infinity === Math.floor(Infinity); // true
Number.isSafeInteger
表示传入的数值有没有精度损失,它比较的是数字是否在 Number.MIN_SAFE_INTEGER
和 Number.MAX_SAFE_INTEGER
之间:
Number.isSafeInteger(Math.pow(2, 53) - 1); // true Number.isSafeInteger(Math.pow(2, 53)); // false
我曾整理过Number的数轴(*),也写过 JavaScript中的一些数字内存模型的demo ,其中有一部分值没有直接的量来表示,但现在有了。
从负无穷往正无穷来看,是这样的:
Number.NEGATIVE_INFINITY
负无穷
-Number.MAX_VALUE
能表示的最小数字,更小被视为负无穷,等于 -(2^53-1)*(2^971)
Number.MIN_SAFE_INTEGER
(新)
没有精度误差的最小数,等于 -(2^53-1)
0
正负零
Number.EPSILON
(新)
IEEE 754规范下的精度位允许的最小差异值,等于 2^-52
Number.MIN_VALUE
能表示的最小正整数,这是一个IEEE 754规范下的反规格化值,等于 2^-1074
Number.MAX_SAFE_INTEGER
(新)
没有精度误差的最大数,,等于 2^53-1
Number.MAX_VALUE
能表示的最大数字,更大被视为正无穷,等于 (2^53-1)*(2^971)
Number.INFINITY
正无穷
比较令人混淆的是 Number.EPSILON
和 Number.MIN_VALUE
,前者为精度位允许的最小差异值,考虑的是浮点数的精度位;而后者考虑的是利用到浮点数的所有位置能够表示的最小正数值。
本节收集了一些奇奇怪怪的错误提示,正常写出的代码不会导致它们,没有兴趣可以略过。
Array.from(1, 2, 3) // Array.of(1,2,3)的误调用 // 2 is not a function
Array.from
、 Promise.all
接口及集合类构造器的参数,可以放入支持迭代器的内容,而不局限于数组(node 0.12+兼容)。这里其实尝试去调用了参数的迭代器 Symbol.iterator
。
Array(); // [] Set(); // Uncaught TypeError: Constructor Set requires 'new'
集合类容器 Int8Array
Uint8Array
Uint8ClampedArray
Int16Array
Uint16Array
Int32Array
Uint32Array
Float32Array
Float64Array
Set
不可以通过非new方式来构造。
var x = 30 `abcdefg` // Uncaught TypeError: 30 is not a function
模版语法可能是ES6最为显然的语法,但它的扩展形式Tagged Template在极端场景可能造成一个奇怪的报错,算是对不写分号党造成的又一个暴击 * 。
本篇章集结一些被滥用的特性。
解构特性很棒,它可以在promise这样的单参数链式调用场景或是正则匹配场景中大方光芒,更为经典的是python风格的 [y, x] = [x, y]
。
但如果一个人铁了心要疯狂解构,新来维护这份代码的人就要默默流下痛苦的眼泪了:
// 新人:是什么阻止了你用 a2 = [o1[a], o1[b], o1[c]] …… var o1 = { a: 1, b: 2, c: 3 }, a2 = []; ( { a: a2[0], b: a2[1], c: a2[2] } = o1 );
// 老人:看得爽吗 var { a: { b: [ c, d ], e: { f } }, g } = obj;
// 主管:写到一半这个程序猿已经被打死了 var x = 200, y = 300, z = 100; var o1 = { x: { y: 42 }, z: { y: z } }; ( { y: x = { y: y } } = o1 ); ( { z: y = { y: z } } = o1 ); ( { x: z = { y: x } } = o1 );
一个可以尝试的保持代码可读性的方法,是尽量保证解构的层次低。
新对象字面量也很不错,新的rest操作符也很实用,但是如果你们把它们混在一起……下面进一段代码赏析( * ):
export const sharePostStatus = createReducer( {}, { [ PUBLICIZE_SHARE ]: ( state, { siteId, postId } ) => ( { ...state, [ siteId ]: { ...state[ siteId ], [ postId ]: { requesting: true, } } } ), [ PUBLICIZE_SHARE_SUCCESS ]: ( state, { siteId, postId } ) => ( { ...state, [ siteId ]: { ...state[ siteId ], [ postId ]: { requesting: false, success: true, } } } ), [ PUBLICIZE_SHARE_FAILURE ]: ( state, { siteId, postId, error } ) => ( { ...state, [ siteId ]: { ...state[ siteId ], [ postId ]: { requesting: false, success: false, error, } } } ), [ PUBLICIZE_SHARE_DISMISS ]: ( state, { siteId, postId } ) => ( { ...state, [ siteId ]: { ...state[ siteId ], [ postId ]: undefined } } ), } );
尽可能的保持代码的可读性,一行只用不超过2个ES6特性或许是一个可操作的方案。