精华 每天一条JS小知识(jstips中文翻译)
发布于 5 年前 作者 sjfkai 16299 次浏览 来自 分享

简介

loverajoel/jstips是最近在github trending里很火的一个repo 。每天会分享一条javascript tip。

我将其翻译成了中文版 目前此项目已有中文版博客。 并把至今的所有tips贴了上来。希望对新手有所帮助

小弟英语也很菜,翻译有误的地方欢迎大家指正或提交PR。

Tips List

#48 - 内置函数Reduce的使用

使用reduce函数时的一些建议

文档里说reduce()方法接收一个函数作为累加器(accumulator),数组中的每个值(从左到右)开始合并,最终为一个值。

reduce()

reduce() 函数接收2个参数(M: 必填, O: 可选):

  • (M) 回调reducer 函数 处理先前的结算结果和下一个元素直到序列结束。
  • (O) 初值 作为第一次调用回调时的第一个参数。

所以,让我们先看一个普通用法,之后再看一个复杂用法。

普通用法 (累加,关联)

我们正在逛亚马逊(单价为美元$) 我们的购物车实在太满了,我们来计算一下总价吧:

// 当前的购物清单
var items = [{price: 10}, {price: 120}, {price: 1000}];

// reducer函数
var reducer = function add(sumSoFar, nextPrice) { return sumSoFar + nextPrice.price; };

// 开始运行
var total = items.reduce(reducer, 0);

console.log(total); // 1130

reduce函数可选的参数在第一个例子里是基本变量数字0,但是它也可以是一个对象,数组… 而不仅是基本类型,之后我们将会看到。

现在,我们收到一个20$的优惠券。

var total = items.reduce(reducer,-20);

console.log(total); // 1110

进阶用法(结合)

第二种用法的例子是ReduxcombineReducers函数源码里用到的。

此创意是将reducer函数拆分为独立的函数,最后组合成一个新的单一的大reducer函数

为了说明,我们创建一个单一的对象,包含一些可以计算不同货币($, €…)的总价值的reducer函数。

var reducers = {
  totalInDollar: function(state, item) {
    state.dollars += item.price;
    return state;
  },
  totalInEuros : function(state, item) {
    state.euros += item.price * 0.897424392;
    return state;
  },
  totalInPounds : function(state, item) {
    state.pounds += item.price * 0.692688671;
    return state;
  },
  totalInYen : function(state, item) {
    state.yens += item.price * 113.852;
    return state;
  }
  // more...
};

然后我们建立一个瑞士军刀函数

  • 能够调用每一部分的reduce函数
  • 返回一个新的reducer回调函数
var combineTotalPriceReducers = function(reducers) {
  return function(state, item) {
    return Object.keys(reducers).reduce(
      function(nextState, key) {
        reducers[key](state, item);
        return state;
      },
      {}      
    );
  }
};

现在,我们来看一下如何使用它。

var bigTotalPriceReducer = combineTotalPriceReducers(reducers);

var initialState = {dollars: 0, euros:0, yens: 0, pounds: 0};

var totals = items.reduce(bigTotalPriceReducer, initialState);

console.log(totals);

/*
Object {dollars: 1130, euros: 1015.11531904, yens: 127524.24, pounds: 785.81131152}
*/

我希望这种方法可以使你在自己的需求内使用reduce()函数时有新的想法。

使用reduce函数也可以实现保存每一次计算结果的功能。这在Ramdajs里的scan函数已经实现了。

在JSFiddle里运行

#47 - 变量声明

理解并应用变量的声明。

下文是JavaScript中声明变量的不同方法。 注释与console.log足够说明这里发生了什么:

var y, x = y = 1 //== var x; var y; x = y = 1
console.log('--> 1:', `x = ${x}, y = ${y}`)

// 将会输出
//--> 1: x = 1, y = 1

首先,我们只设置了两个变量。并没有很多。

;(() => { 
  var x = y = 2 // == var x; y = 2;
  console.log('2.0:', `x = ${x}, y = ${y}`)
})()
console.log('--> 2.1:', `x = ${x}, y = ${y}`)

// 将会输出
//2.0: x = 2, y = 2
//--> 2.1: x = 1, y = 2

正如你所看到的,代码只改变了全局的y,因为我们在闭包里并没有声明此变量。

;(() => { 
  var x, y = 3 // == var x; var y = 3;
  console.log('3.0:', `x = ${x}, y = ${y}`)
})()
console.log('--> 3.1:', `x = ${x}, y = ${y}`)

// 将会输出
//3.0: x = undefined, y = 3
//--> 3.1: x = 1, y = 2

现在我们用var声明了两个变量。意味着他们仅在闭包内有作用。

;(() => { 
  var y, x = y = 4 // == var x; var y; x = y = 4
  console.log('4.0:', `x = ${x}, y = ${y}`)
})()
console.log('--> 4.1:', `x = ${x}, y = ${y}`)

// 将会输出
//4.0: x = 4, y = 4
//--> 4.1: x = 1, y = 2

两个变量都使用var声明了而且在之后又给它们赋值。由于local > global,闭包内声明了xy,意味着闭包内是无法访问全局的xy的。

x = 5 // == x = 5
console.log('--> 5:', `x = ${x}, y = ${y}`)

// 将会输出
//--> 5: x = 5, y = 2

最后一行的结果是很明显的。

更多相关内容请看MDN.

特别感谢@kurtextrem的合作 :)!

#46 - 纯JS监听document是否加载完成

跨浏览器且纯JavaScript检测document是否加载完成。

跨浏览器且纯JavaScript检测document是否加载完成的方法是使用readyState.

if (document.readyState === 'complete') {
	// 页面已完全加载
}

这样可以在document完全加载时监测到……

let stateCheck = setInterval(() => {
	if (document.readyState === 'complete') {
    clearInterval(stateCheck);
	 // document ready
  }
}, 100);

或者使用onreadystatechange

document.onreadystatechange = () => {
  if (document.readyState === 'complete') {
   // document ready
  }
};

使用document.readyState === 'interactive'监听DOM是否加载完成。

#45 - 了解传值机制

对于纯数字数组,使用内置函数Math.max()和Math.min()的方法。

内置函数Math.max()Math.min()可以分别找出参数中的最大值和最小值。

Math.max(1, 2, 3, 4); // 4
Math.min(1, 2, 3, 4); // 1

这些函数对于数字组成的数组是不能用的。但是,这有一些类似地方法。

Function.prototype.apply()让你可以使用提供的this与参数组成的_数组(array)_来调用函数。

var numbers = [1, 2, 3, 4];
Math.max.apply(null, numbers) // 4
Math.min.apply(null, numbers) // 1

apply()第二个参数传递numbers数组,等于使用数组中的所有值作为函数的参数。

一个更简单的,基于ES2015的方法来实现此功能,是使用展开运算符.

var numbers = [1, 2, 3, 4];
Math.max(...numbers) // 4
Math.min(...numbers) // 1

此运算符使数组中的值在函数调用的位置展开。

#44 - 了解传值机制

JavaScript理论上说只传递原始类型和对象(或引用)类型的值。在引用类型的情况下引用值本身通过值传递。

理论上,JavaScript通过值传递。它既不是值传递也不是引用传递,具体取决于它的真实场景。要理解传值机制,看一下下面两个实例代码和解释。

实例 1


var me = {					// 1
	'partOf' : 'A Team'
}; 

function myTeam(me) {		// 2

	me = {					// 3
		'belongsTo' : 'A Group'
	}; 
} 	

myTeam(me);		
console.log(me);			// 4  : {'partOf' : 'A Team'}

在上面的实例里myTeam被调用的时候,JavaScript 传递me对象的引用值,因为它是一个对象。而且调用本身建立了同一个对象的两个独立的引用,(虽然在这里的的命名都是相同的,比如me, 这有些无调行,而且给我们一个这是单个引用的印象)因此,引用变量本身是独立的。

当我们在#3定义了一个新的对象,我们完全改变了myTeam函数内的引用值,这对此函数作用域外的原始对象是没有任何影响的,外作用域的引用仍保留在原始对象上,因此从#4输出去了。

实例 2


var me = {					// 1
	'partOf' : 'A Team'
}; 

function myGroup(me) { 		// 2
	me.partOf = 'A Group';  // 3
} 

myGroup(me);
console.log(me);			// 4  : {'partOf' : 'A Group'}
	

myGroup调用时,我们将对象me传给函数。但是与实例1的情况不同,我们没有指派me变量到任何新对象,有效的说明了myGroup函数作用域内的对象引用值依旧是原始对象的引用值,而且我们在作用域内修改对象的参数值同样有效的修改了原始对象的参数。因此你得到了#7的输出结果。

所以后面的例子是否说明javascript是引用传递呢?不,并没有。请记住,如果是对象的话,JavaScript将引用按值传递。这种混乱往往发生在我们没有完全理解什么通过引用传递的情况下。这就是确切的原因,有些人更愿意称它为call-by-sharing

此文最初被作者发表在js-by-examples

#43 - 函数参数内使用解构

你知道在函数参数内也可以使用解构吗?

大家一定对ES6解构赋值非常熟悉。但是你知道在函数参数里也可以使用它吗?

var sayHello = function({ name, surname }) {
  console.log(`Hello ${name} ${surname}! How are you?`);
};

sayHello({
  name: 'John',
  surname: 'Smith'
});

这对于接收可选参数的函数,是很棒的。

请注意解构赋值在Node.js和大部分浏览器中仍然不可用。但是想自己尝试的话可以在Node.js下使用--harmony-destructuring标记。

#42 - 预防unapply攻击

冻结内置对象的原型方法。

重写内置对象的原型方法,攻击者可以重写代码达到暴漏和修改已绑定参数的函数。这在es5的方法实现polyfill时是一个严重的安全漏洞。

// bind polyfill 示例
function bind(fn) {
  var prev = Array.prototype.slice.call(arguments, 1);
  return function bound() {
    var curr = Array.prototype.slice.call(arguments, 0);
    var args = Array.prototype.concat.apply(prev, curr);
    return fn.apply(null, args);
  };
}


// unapply攻击
function unapplyAttack() {
  var concat = Array.prototype.concat;
  Array.prototype.concat = function replaceAll() {
    Array.prototype.concat = concat; // restore the correct version
    var curr = Array.prototype.slice.call(arguments, 0);
    var result = concat.apply([], curr);
    return result;
  };
}

上面的函数声明忽略了函数bind的prev参数,意味着调用unapplyAttack之后首次调用.concat将会抛出错误。

使用Object.freeze,可以使对象不可变,你可以防止任何内置对象原型方法被重写。

(function freezePrototypes() {
  if (typeof Object.freeze !== 'function') {
    throw new Error('Missing Object.freeze');
  }
  Object.freeze(Object.prototype);
  Object.freeze(Array.prototype);
  Object.freeze(Function.prototype);
}());

你可以在这里阅读更多关于unapply攻击。

#41 - 数组平均值与中值

计算数组的平均值与中位数

下面的例子都基于如下数组:

let values = [2, 56, 3, 41, 0, 4, 100, 23];

要取得平均值,我们需要将数字求和,然后除以values的数目,步骤如下:

  • 取得数组长度(length)
  • 求和(sum)
  • 取得平均值(sum/length)
let values = [2, 56, 3, 41, 0, 4, 100, 23];
let sum = values.reduce((previous, current) => current += previous);
let avg = sum / values.length;
// avg = 28

或者:

let values = [2, 56, 3, 41, 0, 4, 100, 23];
let count = values.length;
values = values.reduce((previous, current) => current += previous);
values /= count;
// avg = 28

取得中值的步骤是:

  • 将数组排序
  • 取得中位数
let values = [2, 56, 3, 41, 0, 4, 100, 23];
values.sort((a, b) => a - b);
let lowMiddle = Math.floor((values.length - 1) / 2);
let highMiddle = Math.ceil((values.length - 1) / 2);
let median = (values[lowMiddle] + values[highMiddle]) / 2;
// median = 13,5

或者使用无符号右移操作符:

let values = [2, 56, 3, 41, 0, 4, 100, 23];
values.sort((a, b) => a - b);
let median = (values[(values.length - 1) >> 1] + values[values.length >> 1]) / 2
// median = 23

#40 - 使用JSON.Stringify

将JSON对象的参数选择性地生成字符串。

加入有一个对象具有参数"prop1", “prop2”, “prop3”。 我们可以通过传递 附加参数JSON.stringify 来选择性将参数生成字符串,像这样:

var obj = {
    'prop1': 'value1',
    'prop2': 'value2',
    'prop3': 'value3'
};

var selectedProperties = ['prop1', 'prop2'];

var str = JSON.stringify(obj, selectedProperties);

// str
// {"prop1":"value1","prop2":"value2"}

“str” 降至包含选择的参数。

除了传递数组,我们也可以传递函数。


function selectedProperties(key, val) {
    // the first val will be the entire object, key is empty string
    if (!key) {
        return val;
    }

    if (key === 'prop1' || key === 'prop2') {
        return val;
    }

    return;
}

最后一个参数,可以修改生成字符串的方式。

var str = JSON.stringify(obj, selectedProperties, '\t\t');

/* str output with double tabs in every line.
{
        "prop1": "value1",
        "prop2": "value2"
}
*/

#39 - Javascript高级特性

怎样给一个对象添加私有参数、gettersetter

在Javascript里配置对象属性是可以实现的,比如将一个参数设为伪私有或者只读。这个特性从ECMAScript 5.1开始就可以使用了,因此近来的浏览器都是支持的。 要实现这些功能,你需要使用Object的原型方法defineProperty,像这样:

var a = {};
Object.defineProperty(a, 'readonly', {
  value: 15,
  writable: false
});

a.readonly = 20;
console.log(a.readonly); // 15

语法如下:

Object.defineProperty(dest, propName, options)

或者定义多个:

Object.defineProperties(dest, {
  propA: optionsA,
  propB: optionsB, //...
})

options包含如下的属性:

  • value: 如果参数不是getter(请看下文),value是必须的。{a: 12} === Object.defineProperty(obj, 'a', {value: 12})
  • writable: 将参数设为只读。需要注意的是如果参数是嵌套对象,它的元素仍是能可修改的。
  • enumerable: 将参数设为隐藏。这意味着for ... of循环和stringify的结果里不会包含这些参数,但是这个参数还是存在的。提示:这并不意味着参数是私有的!他依旧可以从外界访问,只是意味着不会被打印。
  • configurable: 将属性设置为不能更改,比如:防止参数被删除或重新定义。如果此对象是一个嵌套对象,他的参数依旧是可配置的。

所以如果想创建私有静态变量,你可以这样定义:

Object.defineProperty(obj, 'myPrivateProp', {value: val, enumerable: false, writable: false, configurable: false});

除了配置属性,由于defineProperty第二个参数是字符串,所以允许我们定义动态变量(defineProperty)。例如,我们可以说我们要根据一些外部配置创建一个属性:


var obj = {
  getTypeFromExternal(): true // illegal in ES5.1
}

Object.defineProperty(obj, getTypeFromExternal(), {value: true}); // ok

// For the example sake, ES6 introduced a new syntax:
var obj = {
  [getTypeFromExternal()]: true
}

还没有结束!高级特性允许我们创建gettersetter,就像其他面向对象(OOP)语言!这种情况下,我们不能使用writableenumerableconfigurable参数,而是:

function Foobar () {
  var _foo; //  true private property

  Object.defineProperty(obj, 'foo', {
    get: function () { return _foo; }
    set: function (value) { _foo = value }
  });

}

var foobar = new Foobar();
foobar.foo; // 15
foobar.foo = 20; // _foo = 20

除了封装与先进的访问器这些明显的优点,你还发现我们并没有“调用”getter,而是不需要使用小括号直接“取得”了属性!这太棒了!例如,我们可以想象我们有一个多层嵌套对象,像这样:

var obj = {a: {b: {c: [{d: 10}, {d: 20}] } } };

现在我们不需要调用a.b.c[0].d(其中某个属性可能是undefined且抛出错误),我们可以创建一个别名:

Object.defineProperty(obj, 'firstD', {
  get: function () { return a && a.b && a.b.c && a.b.c[0] && a.b.c[0].d }
})

console.log(obj.firstD) // 10

提示

如果你定义了getter而没有定义setter却仍要给它赋值,你将会得到一个错误。这在使用像$.extend_.merge这样的辅助方法时是尤为重要的。要小心!

链接

#38 - Javascript多维数组扁平化

将多位数组转化为单一数组的三种不同方法。

下面是将多位数组转化为单一数组的三种不同方法。

对于此数组:

var myArray = [[1, 2],[3, 4, 5], [6, 7, 8, 9]];

我们需要的结果是:

[1, 2, 3, 4, 5, 6, 7, 8, 9]

解决方案1:使用concat()apply()

var myNewArray = [].concat.apply([], myArray);
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

解决方案2:使用reduce()

var myNewArray = myArray.reduce(function(prev, curr) {
  return prev.concat(curr);
});
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

解决方案3:

var myNewArray3 = [];
for (var i = 0; i < myArray.length; ++i) {
  for (var j = 0; j < myArray[i].length; ++j)
    myNewArray3.push(myArray[i][j]);
}
console.log(myNewArray3);
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

这里看一下三种逻辑的实际作用。

对于无限嵌套的数组试一下Underscore的flatten().

如果你对性能好奇,这里有一个测试。

#37 - 数组去重

移除包含不同类型数据的数组中重复的元素。

原始变量

如果一个数组只包含原始变量,我们可以使用filterindexOf方法将其去重:

var deduped = [ 1, 1, 'a', 'a' ].filter(function (el, i, arr) {
	return arr.indexOf(el) === i;
});

console.log(deduped); // [ 1, 'a' ]

ES2015

我们可以使用箭头函数使写法更简明:

var deduped = [ 1, 1, 'a', 'a' ].filter( (el, i, arr) => arr.indexOf(el) === i);

console.log(deduped); // [ 1, 'a' ]

但是根据Setsfrom方法的介绍,我们可以更简明的实现。

var deduped = Array.from( new Set([ 1, 1, 'a', 'a' ]) );

console.log(deduped); // [ 1, 'a' ]

Objects

当元素为对象(Object)时,我们就不能用这种办法了, 因为对象存储的是引用而原始变量存储的是值。

1 === 1 // true

'a' === 'a' // true

{ a: 1 } === { a: 1 } // false

因此我们需要改变一下我们的实现方法,使用哈希表。

function dedup(arr) {
	var hashTable = {};

	return arr.filter(function (el) {
		var key = JSON.stringify(el);
		var match = Boolean(hashTable[key]);

		return (match ? false : hashTable[key] = true);
	});
}

var deduped = dedup([
	{ a: 1 },
	{ a: 1 },
	[ 1, 2 ],
	[ 1, 2 ]
]);

console.log(deduped); // [ {a: 1}, [1, 2] ]

因为哈希表在Javascript里是一个简单的Object,它的key永远是String类型。这意味着我们不能区分字符串和数字表示的相同的值,如1'1'

var hashTable = {};

hashTable[1] = true;
hashTable['1'] = true;

console.log(hashTable); // { '1': true }

然而,因为我们使用的JSON.stringifyString类型的key 将会被存储为一个字符串值,这样hashTablekey就唯一了。

var hashTable = {};

hashTable[JSON.stringify(1)] = true;
hashTable[JSON.stringify('1')] = true;

console.log(hashTable); // { '1': true, '\'1\'': true }

这意味着相同的值,但不同类型的元素,将以原来的格式保留。

var deduped = dedup([
	{ a: 1 },
	{ a: 1 },
	[ 1, 2 ],
	[ 1, 2 ],
	1,
	1,
	'1',
	'1'
]);

console.log(deduped); // [ {a: 1}, [1, 2], 1, '1' ]

阅读材料

函数

ES2015

Stack overflow

Primitives

如果一个数组至包含原始变量,我们可以使用filterindexOf方法将其去重:

var deduped = [ 1, 1, 'a', 'a' ].filter(function (el, i, arr) {
	return arr.indexOf(el) === i;
});

console.log(deduped); // [ 1, 'a' ]

ES2015

我们可以使用箭头函数使写法更简明:

var deduped = [ 1, 1, 'a', 'a' ].filter( (el, i, arr) => arr.indexOf(el) === i);

console.log(deduped); // [ 1, 'a' ]

但是根据Setsfrom方法的介绍,我们可以更简明的实现。

var deduped = Array.from( new Set([ 1, 1, 'a', 'a' ]) );

console.log(deduped); // [ 1, 'a' ]

Objects

当元素为对象(Object)时,我们就不能用这种办法了, 因为对象存储的是引用而原始变量存储的是值。

1 === 1 // true

'a' === 'a' // true

{ a: 1 } === { a: 1 } // false

因此我们需要改变一下我们的实现方法,使用哈希表。

function dedup(arr) {
	var hashTable = {};

	return arr.filter(function (el) {
		var key = JSON.stringify(el);
		var match = Boolean(hashTable[key]);

		return (match ? false : hashTable[key] = true);
	});
}

var deduped = dedup([
	{ a: 1 },
	{ a: 1 },
	[ 1, 2 ],
	[ 1, 2 ]
]);

console.log(deduped); // [ {a: 1}, [1, 2] ]

因为哈希表在Javascript里是一个简单的Object,它的key永远是String类型。这意味着我们不能区分字符串和数字表示的相同的值,如1'1'

var hashTable = {};

hashTable[1] = true;
hashTable['1'] = true;

console.log(hashTable); // { '1': true }

然而,因为我们使用的JSON.stringifyString类型的key 将会被存储为一个字符串值,这样hashTablekey就唯一了。

var hashTable = {};

hashTable[JSON.stringify(1)] = true;
hashTable[JSON.stringify('1')] = true;

console.log(hashTable); // { '1': true, '\'1\'': true }

这意味着相同的值,但不同类型的元素,将以原来的格式保留。

var deduped = dedup([
	{ a: 1 },
	{ a: 1 },
	[ 1, 2 ],
	[ 1, 2 ],
	1,
	1,
	'1',
	'1'
]);

console.log(deduped); // [ {a: 1}, [1, 2], 1, '1' ]

阅读材料

函数

ES2015

Stack overflow

#36 - 扩展插件中观察DOM的变化

当你为存在的网站开发扩展插件时,由于现代的动态Javascript,操作DOM并不是很容易。

MutationObserver是监听DOM变化与当元素变化时做适当操作的一个解决方法。在下面的例子中我们使用计时器模拟了内容的动态加载,第一个元素"target"创建后,创建"subTarget"。 在扩展中的代码,rootObserver首先开始工作,直到targetElement被创建后rootObserver停止,然后elementObserver开始工作。这个级联观测可以在发现subTargetElement时提醒你。 这个方法在为动态加载内容的网站开发扩展插件时,是很有用的。

const observeConfig = {
    attributes: true,
    childList: true,
    characterData: true,
    subtree: true
};

function initExtension(rootElement, targetSelector, subTargetSelector) {
    var rootObserver = new MutationObserver(function(mutations) {
        console.log("Inside root observer");
        targetElement = rootElement.querySelector(targetSelector);
        if (targetElement) {
            rootObserver.disconnect();
            var elementObserver = new MutationObserver(function(mutations) {
                console.log("Inside element observer")
                subTargetElement = targetElement.querySelector(subTargetSelector);
                if (subTargetElement) {
                    elementObserver.disconnect();
                    console.log("subTargetElement found!")
                }
            })
            elementObserver.observe(targetElement, observeConfig);
        }
    })
    rootObserver.observe(rootElement, observeConfig);
}

(function() {

    initExtension(document.body, "div.target", "div.subtarget")

    setTimeout(function() {
        del = document.createElement("div");
        del.innerHTML = "<div class='target'>target</div>"
        document.body.appendChild(del)
    }, 3000);


    setTimeout(function() {
        var el = document.body.querySelector('div.target')
        if (el) {
            del = document.createElement("div");
            del.innerHTML = "<div class='subtarget'>subtarget</div>"
            el.appendChild(del)
        }
    }, 5000);

})()

#35 - 赋值技巧

赋值是很常见的。有时候打字对于我们这些“懒惰的程序员”来说是很费时间的。 所以,我们可以使用一些小把戏来使我们的代码更清楚更简单。

这类似于使用:

x += 23; // x = x + 23;
y -= 15; // y = y - 15;
z *= 10; // z = z * 10;
k /= 7; // k = k / 7;
p %= 3; // p = p % 3;
d **= 2; // d = d ** 2;
m >>= 2; // m = m >> 2;
n <<= 2; // n = n << 2;
n ++; // n = n + 1;
n --; n = n - 1;

If-else (使用三元运算符)

我们平时会这样写:

var newValue;
if(value > 10) 
  newValue = 5;
else
  newValue = 2;

我们可以使用三元运算符是它更简便:

var newValue = (value > 10) ? 5 : 2;

检测Null、Undefined、空

if (variable1 !== null || variable1 !== undefined || variable1 !== '') {
     var variable2 = variable1;
}

简便写法:

var variable2 = variable1  || '';

P.S.:如果variable1是一个数组,则先检查他是否为0。

对象数组表示法

不要用:

var a = new Array();
a[0] = "myString1";
a[1] = "myString2";

使用:

var a = ["myString1", "myString2"];

关联数组

不要用:

var skillSet = new Array();
skillSet['Document language'] = 'HTML5';
skillSet['Styling language'] = 'CSS3';

使用:

var skillSet = {
    'Document language' : 'HTML5', 
    'Styling language' : 'CSS3'
};

#34 - 实现异步循环

实现异步循环时,你可能会遇到问题。

让我们试着写一个异步方法,每秒打印一次循环的索引值。

for (var i=0; i<5; i++) {
	setTimeout(function(){
		console.log(i); 
	}, 1000);
}  

如上程序的输出为:

> 5
> 5
> 5
> 5
> 5

这明显是有问题的。

原因

每次时间结束(timeout)都指向原始的i,而并非它的拷贝。所以,for循环使i增长到5,之后timeout运行并调用了当前i的值(也就是5)。

好吧,这个问题看起来很简单。最直接的解决方法是将循环的索引缓存在一个临时变量里。

for (var i=0; i<5; i++) {
	var temp = i;
 	setTimeout(function(){
		console.log(temp); 
	}, 1000);
}  

但是再次运行,如上的程序输出为:

> 4
> 4
> 4
> 4
> 4

这仍然有问题,这是因为并不存在块作用域,而且变量的声明被提升到了作用域顶端。实际上,如上代码和下面是一样的:

var temp;
for (var i=0; i<5; i++) {
 	temp = i;
	setTimeout(function(){
		console.log(temp); 
  	}, 1000);
}  

解决方法

有几个不同的方式可以拷贝i。最普通且常用方法是通过声明函数来建立一个闭包,并将i传给此函数。我们这里使用了自调用函数。

for (var i=0; i<5; i++) {
	(function(num){
		setTimeout(function(){
			console.log(num); 
		}, 1000); 
	})(i);  
}  

在JavaScript里,参数是按值传递给函数的。像NumberDateString这些原始类型为基本复制。当你们在一个函数内改变它的值,并不影响外面的作用域。但Object类型不一样:如果你在函数内部修改了它的参数,将会影响到所有包含该Object的作用域内它的参数。

另一种方法是使用let。在ES6中的let关键字是可以实现的,它和var不一样,因为它支持块作用域的。

for (let i=0; i<5; i++) {
	var temp = i;
 	setTimeout(function(){
		console.log(temp); 
	}, 1000);
}  

#33 - 仅用一行生成0…N的数列

我们可以创建一个函数,它可以仅用一行代码生成0…N数列。

使用下面一行代码,我们就可以生成0…N数列。

Array.apply(null, {length: N}).map(Number.call, Number);

让我们把这一行拆分一下。我们知道"call"方法在Javascript中的作用。"call"方法的第一个参数是上下文,从第二个参数开始是调用"call"方法的函数所需要的参数。

function add(a,b){
    return (a+b);
}
add.call(null,5,6);

这将返回5加6的和。

数组的map接收两个参数,第一个是回调函数(callback),第二个是上下文(this)。回调函数接收三个参数:valueindex和我们正在迭代的整个数组。所以正常的语法就像:

[1,2,3].map(function(val,index,arr){
    //Code
},this);

如下一行创建了一个所给长度(length)的数组:

Array.apply(null, {length: N})

将各部分合并就成了如下解决方案:

Array.apply(null, {length: N}).map(Number.call, Number);

如果你需要1…N的数列,你可以这样写:

Array.apply(null, {length: N}).map(function(val,index){
  return index+1;  
});

#32 - Map()的营救;使对象属性有顺序

对象是一个无序的对象集合。这意味着如果你想在对象里保存有序的数据,你需要重新处理它,因为对象里的数据不保证是有序的。

对象属性顺序

一个对象是一个Object类型的实例。它是由一些未排序的元素组成的集合,其中包含了原始变量,对象,和函数。一个对象的属性所对应的函数被称为方法。ECMAScript

实际看一下

var myObject = {
	z: 1,
	'@': 2,
	b: 3,
	1: 4,
	5: 5
};
console.log(myObject) // Object {1: 4, 5: 5, z: 1, @: 2, b: 3}

for (item in myObject) {...
// 1
// 5
// z
// @
// b

因为技术实现,每个浏览器在排序时都有自己的规则,顺序是不确定的。

怎么解决呢?

Map

使用ES6的新特性Map。Map 对象以插入的顺序遍历元素。for...of循环为每一次循环返回一个[key, value]数组。

var myObject = new Map();
myObject.set('z', 1);
myObject.set('@', 2);
myObject.set('b', 3);
for (var [key, value] of myObject) {
  console.log(key, value);
...
// z 1
// @ 2
// b 3

攻克老浏览器

Mozilla 建议:

所以,如果过你想在跨浏览器环境中模拟一个有序的关联数组,你要么使用两个分开的数组(一个保存key,另一个保存value),要么构建一个单属性对象(single-property objects)的数组。

// 使用分开的数组
var objectKeys = [z, @, b, 1, 5];
for (item in myObject) {
	myObject[item]
...

// 构建一个单属性对象(single-property objects)的数组
var myData = [{z: 1}, {'@': 2}, {b: 3}, {1: 4}, {5: 5}];

#31 - 避免修改和传递arguments给其他方法 — 影响优化

背景

在JavaScript的方法里,arguments参数可以让你访问传递给该方法的所有参数。arguments是一个类数组对象arguments可是使用数组标记访问,而且它有length参数,但是它没有filtermapforEach这样内建到数组内的方法。因此,如下代码是一个非常常见的将arguments转换为数组的办法:

var args = Array.prototype.slice.call(arguments);

arguments传递给Array原型(prototype)上的slice方法;slice方法返回一个对arguments浅复制后的数组对象。更短的写法:

var args = [].slice.call(arguments);

在这里,简单的调用了空数组的slice方法,而没有从Array的原型(prototype)上调用。

系统优化

不幸的是,传递arguments给任何参数,将导致Chrome和Node中使用的V8引擎跳过对其的优化,这也将使性能相当慢。看一下这篇文章optimization killers。传递arguments给任何方法被称为leaking arguments

如果你想用一个包含参数(arguments)的数组,你需要求助于这个办法:

var args = new Array(arguments.length);
for(var i = 0; i < args.length; ++i) {
  args[i] = arguments[i];
}

没错,这很啰嗦,但是在生产环境中的代码里,为了系统性能优化,这是值得的。

#30 - 将truthy/falsy转换为布尔值

你可以使用!!操作符将truthyfalsy值转换为布尔值。

!!"" // false
!!0 // false
!!null // false
!!undefined // false
!!NaN // false

!!"hello" // true
!!1 // true
!!{} // true
!![] // true

#29 - 运用存储加速递归

大家对斐波那契(Fibonacci)数列都很熟悉。我们可以再20秒内写出下面这样一个方法。

var fibonacci = function(n){
    return n < 2 ? n : fibonacci(n-1) + fibonacci(n-2);
}

它可以运行,但并不高效。它做了太多重复的运算,我们可以通过存储这些运算结果来使其加速。 (译者注:此段代码有问题 看这里)

var fibonacci = (function(){
    var cache = {
        0: 0,
        1: 1
    };
    return function self(n){
        return n <= 1 ? cache[n] : (cache[n] = self(n-1) + self(n-2));
    }
})()

我们也可以定义一个高阶函数,它接收一个方法作为参数,返回一个该方法运用存储后的新方法。

var memoize = function(func){
    var cache = {};
    return function(){
        var key = Array.prototype.slice.call(arguments).toString();
        return key in cache ? cache[key] : (cache[key] = func.apply(this,arguments));
    }
}
fibonacci = memoize(fibonacci);

我们可以将memoize()用在很多其他地方

  • GCD(最大公约数)
var gcd = memoize(function(a,b){
    var t;
    if (a < b) t=b, b=a, a=t;
    while(b != 0) t=b, b = a%b, a=t;
    return a;
})
gcd(27,183); //=> 3
  • 阶乘运算
var factorial = memoize(function(n) {
    return (n <= 1) ? 1 : n * factorial(n-1);
})
factorial(5); //=> 120

#28 - 柯里化(currying)与部分应用(partial application)

柯里化(currying)

柯里化是使一个函数

f: X * Y -> R

转变为

f’: X -> (Y -> R)

与用两个参数调用f不同,我们用一个参数运行f’。返回的结果是一个函数,然后用第二个参数调用此函数,得到结果。

如此,如果未柯里化的函数f这样调用

f(3,5)

柯里化后的函数f’是这样调用的

f(3)(5)

比如: 未柯里化的函数add()


function add(x, y) {
  return x + y;
}

add(3, 5);   // returns 8

柯里化后的add()

function addC(x) {
  return function (y) {
    return x + y;
  }
}

addC(3)(5);   // returns 8

柯里化的规则

柯里化将一个二元函数,转变为一元函数,这个函数将返回另一个一元函数。

curry: (X × Y → R) → (X → (Y → R))

Javascript Code:

function curry(f) {
  return function(x) {
    return function(y) {
      return f(x, y);
    }
  }
}

部分应用(partial application)

部分应用将一个函数

f: X * Y -> R

的第一个参数固定而产生一个新的函数

f`: Y -> R

f’与f不同,只需要填写第二个参数,这也是f’比f少一个参数的原因。

比如:将函数add的第一个参数绑定为5来产生函数plus5。

function plus5(y) {
  return 5 + y;
}

plus5(3);  // returns 8

部分应用的规则

部分应用使用一个二元函数和一个值产生了一个一元函数。

partApply : ((X × Y → R) × X) → (Y → R)

Javascript Code:

function partApply(f, x) {
  return function(y) {
    return f(x, y);
  }
}

#27 - JS中的短路求值

短路求值是说, 只有当第一个运算数的值无法确定逻辑运算的结果时,才对第二个运算数进行求值:当AND(&&)的第一个运算数的值为false时,其结果必定为false;当OR(||)的第一个运算数为true时,最后结果必定为true。

对于下面的test条件和isTrueisFalse方法

var test = true;
var isTrue = function(){
  console.log('Test is true.');
};
var isFalse = function(){
  console.log('Test is false.');
};

使用逻辑与 - &&.

// 普通的if语句
if(test){
  isTrue();    // Test is true
}

// 上面的语句可以使用 '&&' 写为:

( test && isTrue() );  // Test is true

使用逻辑或 - ||.

test = false;
if(!test){
  isFalse();    // Test is false.
}

( test || isFalse());  // Test is false.

逻辑或可以用来给参数设置默认值。

function theSameOldFoo(name){ 
    name = name || 'Bar' ;
    console.log("My best friend's name is " + name);
}
theSameOldFoo();  // My best friend's name is Bar
theSameOldFoo('Bhaskar');  // My best friend's name is Bhaskar

逻辑与可以用来避免调用undefined参数的属性时报错 例如:-

var dog = { 
  bark: function(){
     console.log('Woof Woof');
   }
};

// 调用 dog.bark();
dog.bark(); // Woof Woof.

// 但是当dog未定义时,dog.bark() 将会抛出"Cannot read property 'bark' of undefined." 错误
// 防止这种情况,我们可以使用 &&.

dog&&dog.bark();   // This will only call dog.bark(), if dog is defined.

#26 - 过滤并排序字符串列表

你可能有一个很多名字组成的列表,需要过滤掉重复的名字并按字母表将其排序。

在我们的例子里准备用不同版本语言的JavaScript 保留字的列表,但是你能发现,有很多重复的关键字而且它们并没有按字母表顺序排列。所以这是一个完美的字符串列表(数组)来测试我们的JavaScript小知识。

var keywords = ['do', 'if', 'in', 'for', 'new', 'try', 'var', 'case', 'else', 'enum', 'null', 'this', 'true', 'void', 'with', 'break', 'catch', 'class', 'const', 'false', 'super', 'throw', 'while', 'delete', 'export', 'import', 'return', 'switch', 'typeof', 'default', 'extends', 'finally', 'continue', 'debugger', 'function', 'do', 'if', 'in', 'for', 'int', 'new', 'try', 'var', 'byte', 'case', 'char', 'else', 'enum', 'goto', 'long', 'null', 'this', 'true', 'void', 'with', 'break', 'catch', 'class', 'const', 'false', 'final', 'float', 'short', 'super', 'throw', 'while', 'delete', 'double', 'export', 'import', 'native', 'public', 'return', 'static', 'switch', 'throws', 'typeof', 'boolean', 'default', 'extends', 'finally', 'package', 'private', 'abstract', 'continue', 'debugger', 'function', 'volatile', 'interface', 'protected', 'transient', 'implements', 'instanceof', 'synchronized', 'do', 'if', 'in', 'for', 'let', 'new', 'try', 'var', 'case', 'else', 'enum', 'eval', 'null', 'this', 'true', 'void', 'with', 'break', 'catch', 'class', 'const', 'false', 'super', 'throw', 'while', 'yield', 'delete', 'export', 'import', 'public', 'return', 'static', 'switch', 'typeof', 'default', 'extends', 'finally', 'package', 'private', 'continue', 'debugger', 'function', 'arguments', 'interface', 'protected', 'implements', 'instanceof', 'do', 'if', 'in', 'for', 'let', 'new', 'try', 'var', 'case', 'else', 'enum', 'eval', 'null', 'this', 'true', 'void', 'with', 'await', 'break', 'catch', 'class', 'const', 'false', 'super', 'throw', 'while', 'yield', 'delete', 'export', 'import', 'public', 'return', 'static', 'switch', 'typeof', 'default', 'extends', 'finally', 'package', 'private', 'continue', 'debugger', 'function', 'arguments', 'interface', 'protected', 'implements', 'instanceof'];

因为我们不想改变我们的原始列表,所以我们准备用高阶函数叫做filter,它将基于我们传递的回调方法返回一个新的过滤后的数组。回调方法将比较当前关键字在原始列表里的索引和新列表中的索引,仅当索引匹配时将当前关键字push到新数组。

最后我们准备使用sort方法排序过滤后的列表,sort只接受一个比较方法作为参数,并返回按字母表排序后的列表。

var filteredAndSortedKeywords = keywords
  .filter(function (keyword, index) {
      return keywords.indexOf(keyword) === index;
    })
  .sort(function (a, b) {
      if (a < b) return -1;
      else if (a > b) return 1;
      return 0;
    });

ES6 (ECMAScript 2015)版本下使用箭头函数看起来更简单:

const filteredAndSortedKeywords = keywords
  .filter((keyword, index) => keywords.indexOf(keyword) === index)
  .sort((a, b) => {
      if (a < b) return -1;
      else if (a > b) return 1;
      return 0;
    });

这是最后过滤和排序后的JavaScript保留字列表:

console.log(filteredAndSortedKeywords);

// ['abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'final', 'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', 'int', 'interface', 'let', 'long', 'native', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'short', 'static', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', 'typeof', 'var', 'void', 'volatile', 'while', 'with', 'yield']

#25 -使用立即执行函数表达式

立即执行函数表达式( IIFE - immediately invoked function expression)是一个立即执行的匿名函数表达式,它在Javascript中有一些很重要的用途。


(function() {
 // Do something​
 }
)()

这是一个立即执行的匿名函数表达式,它在有JavaScript一些特别重要的用途。

两对括号包裹着一个匿名函数,使匿名函数变成了一个函数表达式。于是,我们现在拥有了一个未命名的函数表达式,而不是一个全局作用域下或在任何地方定义的的简单函数。

类似地,我们也可以创建一个命名过的立即执行函数表达式:

(someNamedFunction = function(msg) {
	console.log(msg || "Nothing for today !!")
	}) (); // 输出 --> Nothing for today !!​
​
someNamedFunction("Javascript rocks !!"); // 输出 --> Javascript rocks !!
someNamedFunction(); // 输出 --> Nothing for today !!​

更多内容, 请参考下面链接 -

  1. 链接 1
  2. 链接 2

效率: jsPerf

#24 - 使用 === 而不是 ==

== (或者 !=) 操作在需要的情况下自动进行了类型转换。=== (或 !==)操作不会执行任何转换。===在比较值和类型时,可以说比==更快(jsPref)。

[10] ==  10      // 为 true
[10] === 10      // 为 false

'10' ==  10      // 为 true
'10' === 10      // 为 false

 []  ==  0       // 为 true
 []  === 0       // 为 false

 ''  ==  false   // 为 true 但 true == "a" 为false
 ''  === false   // 为 false 

#23 - 转换为数字的更快方法(原文)

2016-01-23 by @sonnyt

将字符串转换为数字是极为常见的。最简单和快速的方法(jsPref)+(加号) 来实现。

var one = '1';

var numberOne = +one; // Number 1

你也可以用-(减号)将其转化为负数值。

var one = '1';

var negativeNumberOne = -one; // Number -1

#22 - 清空数组

2016-01-22 by microlv

如果你定义了一个数组,然后你想清空它。 通常,你会这样做:

// 定义一个数组
var list = [1, 2, 3, 4];
function empty() {
    //清空数组
    list = [];
}
empty();

但是,这有一个效率更高的方法来清空数组。 你可以这样写:

var list = [1, 2, 3, 4];
function empty() {
    //empty your array
    list.length = 0;
}
empty();
  • list = [] 将一个新的数组的引用赋值给变量,其他引用并不受影响。 这意味着以前数组的内容被引用的话将依旧存在于内存中,这将导致内存泄漏。

  • list.length = 0 删除数组里的所有内容,也将影响到其他引用。

然而,如果你复制了一个数组(A 和 Copy-A),如果你用list.length = 0清空了它的内容,复制的数组也会清空它的内容。

考虑一下将会输出什么:

var foo = [1,2,3];
var bar = [1,2,3];
var foo2 = foo;
var bar2 = bar;
foo = [];
bar.length = 0;
console.log(foo, bar, foo2, bar2);

//[] [] [1, 2, 3] []

更多内容请看Stackoverflow: difference-between-array-length-0-and-array

#21 - 对数组洗牌

2016-01-21 by @0xmtn

这段代码运用了Fisher-Yates Shuffling算法对数组进行洗牌。

function shuffle(arr) {
    var i, 
        j,
        temp;
    for (i = arr.length - 1; i > 0; i--) {
        j = Math.floor(Math.random() * (i + 1));
        temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    return arr;    
};

调用示例:

var a = [1, 2, 3, 4, 5, 6, 7, 8];
var b = shuffle(a);
console.log(b);
// [2, 7, 8, 6, 5, 3, 1, 4]

#20 - 返回对象,使方法可以链式调用(原文)

2016-01-20 by @WakeskaterX

在面向对象的Javascript中为对象建立一个方法时,返回当前对象可以让你在一条链上调用方法。

function Person(name) {
  this.name = name;

  this.sayName = function() {
    console.log("Hello my name is: ", this.name);
    return this;
  };

  this.changeName = function(name) {
    this.name = name;
    return this;
  };
}

var person = new Person("John");
person.sayName().changeName("Timmy").sayName();

#19 - 安全的字符串拼接(原文)

2016-01-19 by @gogainda

假如你需要拼接一些不确定类型的变量为字符串,你需要确保算术运算符在你拼接时不会起作用。

var one = 1;
var two = 2;
var three = '3';

var result = ''.concat(one, two, three); //"123"

这应该就是你所期望的拼接结果。如果不这样,拼接时加号可能会导致你意想不到的结果:

var one = 1;
var two = 2;
var three = '3';

var result = one + two + three; //"33" instead of "123"

关于性能,与用join来拼接字符串相比 concat的效率是几乎一样的。

你可以在MDN了解更多关于concat方法的内容。

#18 - 更快的取整(原文)

2016-01-18 by pklinger

今天的小知识有关性能表现。曾经用过双波浪 ~~ 操作符吗? 有时候也称为双非(NOT)位操作. 你可以用它作为Math.floor()的替代方法。为什么会这样呢?

一个位操作符 ~ 将输入的32位的数字(input)转换为 -(input+1). 两个位操作符将输入(input)转变为 -(-(input + 1)+1) 是一个使结果趋向于0的取整好工具. 对于数字, 负数就像使用Math.ceil()方法而正数就像使用Math.floor()方法. 转换失败时,返回0,这在Math.floor()方法转换失败返回NaN时或许会派上用场。

// 单个 ~
console.log(~1337)    // -1338

// 数字输入
console.log(~~47.11)  // -> 47
console.log(~~-12.88) // -> -12
console.log(~~1.9999) // -> 1
console.log(~~3)      // -> 3

// 转换失败
console.log(~~[]) // -> 0
console.log(~~NaN)  // -> 0
console.log(~~null) // -> 0

// 大于32位整数时转换失败
console.log(~~(2147483647 + 1) === (2147483647 + 1)) // -> 0

虽然~~的性能更好,为了代码的可读性请用Math.floor().

#17 - Node.js: 运行未被引用的模块(原文)

2016-01-17 by @odsdq

在Node里,你可以让你的程序根据其运行自require('./something.js')或者node something.js而做不同的处理。如果你想与你的一个独立的模块进行交互,这是非常有用的。

if (!module.parent) {
    // 通过 `node something.js` 启动
    app.listen(8088, function() {
        console.log('app listening on port 8088');
    })
} else {
    // 通过 `require('/.something.js')` 被引用
    module.exports = app;
}

更多内容请看 modules的文档

#16 - 向回调方法传递参数(原文)

2016-01-16 by @minhazav

通常下,你并不能给回调函数传递参数。 比如:

function callback() {
  console.log('Hi human');
}

document.getElementById('someelem').addEventListener('click', callback);

你可以借助Javascript闭包的优势来传递参数给回调函数。看这个例子:

function callback(a, b) {
  return function() {
    console.log('sum = ', (a+b));
  }
}

var x = 1, y = 2;
document.getElementById('someelem').addEventListener('click', callback(x, y));

什么是闭包? 闭包是指函数有自由独立的变量。换句话说,定义在闭包中的函数可以“记忆”它创建时候的环境。想了解更多请参考MDN的文档

这种方法使参数xy在回调方法被调用时处于其作用域内。

另一个办法是使用bind方法。比如:

var alertText = function(text) {
  alert(text);
};

document.getElementById('someelem').addEventListener('click', alertText.bind(this, 'hello'));

两种方法之间有着微小的性能差异,请看jsperf.

#15 - 更简单的使用indexOf实现contains功能(原文)

2016-01-15 by @jhogoforbroke

JavaScript并未提供contains方法。检测子字符串是否存在于字符串或者变量是否存在于数组你可能会这样做:

var someText = 'javascript rules';
if (someText.indexOf('javascript') !== -1) {
}

// or
if (someText.indexOf('javascript') >= 0) {
}

但是让我们看一下这些 Expressjs代码段。

examples/mvc/lib/boot.js

for (var key in obj) {
  // "reserved" exports
  if (~['name', 'prefix', 'engine', 'before'].indexOf(key)) continue;

lib/utils.js

exports.normalizeType = function(type){
  return ~type.indexOf('/')
    ? acceptParams(type)
    : { value: mime.lookup(type), params: {} };
};

examples/web-service/index.js

// key is invalid
if (!~apiKeys.indexOf(key)) return next(error(401, 'invalid api key'));

难点是 位操作符 ~, “按位操作符操作数字的二进制形式,但是返回值依然是标准的JavaScript数值。”

它将-1转换为0,而0在javascript为false,所以:

var someText = 'text';
!!~someText.indexOf('tex'); // someText contains "tex" - true
!~someText.indexOf('tex'); // someText NOT contains "tex" - false
~someText.indexOf('asd'); // someText doesn't contain "asd" - false
~someText.indexOf('ext'); // someText contains "ext" - true

String.prototype.includes()

在ES6中提供了includes() 方法供我们判断一个字符串是否包含了另一个字符串:

'something'.includes('thing'); // true

在ECMAScript 2016 (ES7)甚至可能将其应用于数组,像indexOf一样:

!!~[1, 2, 3].indexOf(1); // true
[1, 2, 3].includes(1); // true

不幸的是, 只有Chrome、Firefox、Safari 9及其更高版本和Edge支持了这功能。IE11及其更低版本并不支持 最好在受控的环境中使用此功能

#14 - 箭头函数 #ES6(原文)

2016-01-13 by @pklinger

介绍一个ES6的新特性,箭头函数或许一个让你用更少行写更多代码的方便工具。它的名字(fat arrow functions)来自于它的语法=>是一个比瘦箭头->要’胖的箭头’(译者注:但是国内貌似不分胖瘦就叫箭头函数)。Some programmers might already know this type of functions from different languages such as Haskell as ‘lambda expressions’ respectively ‘anonymous functions’. It is called anonymous, as these arrow functions do not have a descriptive function name.(译者注:一些其他语言中的箭头函数,避免不准确就不翻译了)

有什么益处呢?

  • 语法: 更少的代码行; 不再需要一遍一遍的打function
  • 语义: 从上下文中捕获this关键字

简单的语法示例

观察一下这两个功能完全相同的代码片段。你将很快明白箭头函数做了什么。

// 箭头函数的一般语法
param => expression

// 也可以用用小括号
// 多参数时小括号是必须的
(param1 [, param2]) => expression


// 使用functions
var arr = [5,3,2,9,1];
var arrFunc = arr.map(function(x) {
  return x * x;
});
console.log(arr)

// 使用箭头函数
var arr = [5,3,2,9,1];
var arrFunc = arr.map((x) => x*x);
console.log(arr)

正如你所看到的,箭头函数在这种情况下省去了写小括号,function以及return的时间。我建议你总是使用小括号,因为对于像(x,y) => x+y这样多参数函数,小括号总是需要的。这仅是以防在不同使用场景下忘记小括号的一种方法。但是上面的代码和x => x*x是一样的。至此仅是语法上的提升,减少了代码行数并提高了可读性。

Lexically binding this

这是另一个使用箭头函数的好原因。这是一个关于this上下文的问题。使用箭头函数,你不需要再担心.bind(this)也不用再设置that = this了,因为箭头函数继承了外围作用域的this值。看一下下面的示例:


// 全局定义 this.i
this.i = 100;

var counterA = new CounterA();
var counterB = new CounterB();
var counterC = new CounterC();
var counterD = new CounterD();

// 不好的例子
function CounterA() {
  // CounterA的`this`实例 (!!调用时忽略了此实例)
  this.i = 0;
  setInterval(function () {
    // `this` 指向全局(global)对象,而不是CounterA的`this`
    // 所以从100开始计数,而不是0 (本地的this.i)
    this.i++;
    document.getElementById("counterA").innerHTML = this.i;
  }, 500);
}

// 手动绑定 that = this
function CounterB() {
  this.i = 0;
  var that = this;
  setInterval(function() {
    that.i++;
    document.getElementById("counterB").innerHTML = that.i;
  }, 500);
}

// 使用 .bind(this)
function CounterC() {
  this.i = 0;
  setInterval(function() {
    this.i++;
    document.getElementById("counterC").innerHTML = this.i;
  }.bind(this), 500);
}

// 箭头函数
function CounterD() {
  this.i = 0;
  setInterval(() => {
    this.i++;
    document.getElementById("counterD").innerHTML = this.i;
  }, 500);
}

更多有关箭头函数的内容可以查看MDN。更多语法选项请看这里.

#13 - 测量javascript代码块性能的小知识(原文)

2016-01-13 by @manmadareddy

快速的测量javascript的性能,我们可以使用console的方法,例如 console.time(label)console.timeEnd(label)

console.time("Array initialize");
var arr = new Array(100),
    len = arr.length,
    i;

for (i = 0; i < len; i++) {
    arr[i] = new Object();
};
console.timeEnd("Array initialize"); // Outputs: Array initialize: 0.711ms

更多内容: Console object, Javascript benchmarking

Demo: jsfiddle - codepen (在浏览器控制台输出)

#12 - ES6中的伪强制参数 #ES6(原文)

2016-01-12 by Avraam Mavridis

在许多编程语言中,方法的参数时默认强制需要的,开发人员需要明确定义一个可选的参数。在Javascript中任何参数都是可选的,但是我们可以利用es6参数默认值特性的优点来实现强制要求这种表现而不污染本身的函数体。

const _err = function( message ){
  throw new Error( message );
}

const getSum = (a = _err('a is not defined'), b = _err('b is not defined')) => a + b

getSum( 10 ) // throws Error, b is not defined
getSum( undefined, 10 ) // throws Error, a is not defined

_err方法会立即抛出一个错误。如果没有传递值给参数,默认值将会被使用, _err方法将被调用而错误也将被抛出。你可以从MDN看到更多关于默认参数特性的例子。

#11 - 变量提升(原文)

2016-01-11 by @squizzleflip

了解变量提升可以帮助你组织方法作用域。只要记住变量生命和方法生命都会被提升到顶部。变量的定义不会提升,即使你在同一行声明和定义一个变量。变量声明是让系统知道有这个变量存在而定义是给其赋值。

function doTheThing() {
  // ReferenceError: notDeclared is not defined
  console.log(notDeclared);

  // Outputs: undefined
  console.log(definedLater);
  var definedLater;

  definedLater = 'I am defined!'
  // Outputs: 'I am defined!'
  console.log(definedLater)

  // Outputs: undefined
  console.log(definedSimulateneously);
  var definedSimulateneously = 'I am defined!'
  // Outputs: 'I am defined!'
  console.log(definedSimulateneously)

  // Outputs: 'I did it!'
  doSomethingElse();

  function doSomethingElse(){
    console.log('I did it!');
  }

  // TypeError: undefined is not a function
  functionVar();

  var functionVar = function(){
    console.log('I did it!');
  }
}

为了让你的代码更易读,将所有的变量声明在函数的顶端,这样可以更清楚的知道变量来自哪个作用域。在使用变量之前声明变量。将方法定义在函数的底部。

#10 - 检查某对象是否有某属性(原文)

2016-01-10 by @loverajoel

当你需要检查某属性是否存在于一个对象,你可能会这样做:

var myObject = {
  name: '[@tips_js](/user/tips_js)'
};

if (myObject.name) { ... }

这是可以的,但是你需要知道有两种原生方法可以解决此类问题。in 操作符Object.hasOwnProperty,任何继承自Object的对象都可以使用这两种方法。

看一下较大的区别

var myObject = {
  name: '[@tips_js](/user/tips_js)'
};

myObject.hasOwnProperty('name'); // true
'name' in myObject; // true

myObject.hasOwnProperty('valueOf'); // false, valueOf 继承自原型链
'valueOf' in myObject; // true

两者检查属性的深度不同,换言之hasOwnProperty只在本身有此属性时返回true,而in操作符不区分属性来自于本身或继承自原型链。

这是另一个例子

var myFunc = function() {
  this.name = '[@tips_js](/user/tips_js)';
};
myFunc.prototype.age = '10 days';

var user = new myFunc();

user.hasOwnProperty('name'); // true
user.hasOwnProperty('age'); // false, 因为age来自于原型链

在线示例!

同样建议阅读关于检查对象是否包含属性时常见错误的讨论

#09 - 模板字符串

2016-01-09 by @JakeRawr(原文)

ES6中,JS现在有了引号拼接字符串的替代品,模板字符串。

示例: 普通字符串

var firstName = 'Jake';
var lastName = 'Rawr';
console.log('My name is ' + firstName + ' ' + lastName);
// My name is Jake Rawr

模板字符串

var firstName = 'Jake';
var lastName = 'Rawr';
console.log(`My name is ${firstName} ${lastName}`);
// My name is Jake Rawr

在模板字符串中,你可以不用\n来生成多行字符串还可以在${}里做简单的逻辑运算(例如 2+3)。

你也可以使用方法修改模板字符串的输出内容;他们被称为带标签的模板字符串(其中有带标签的模板字符串的示例)

或许你还想阅读更多内容来了解模板字符串。

#08 - 将Node List转换为数组(Array)(原文)

2016-01-08 by @Tevko

querySelectorAll方法返回一个类数组对象称为node list。这些数据结构被称为“类数组”,因为他们看似数组却没有类似mapforeach这样的数组方法。这是一个快速、安全、可重用的方法将node list转换为DOM元素的数组:

const nodelist = document.querySelectorAll('div');
const nodelistToArray = Array.apply(null, nodelist);

//之后 ..

nodelistToArray.forEach(...);
nodelistToArray.map(...);
nodelistToArray.slice(...);

//等...

apply方法可以在指定this时以数组形式向方法传递参数。MDN规定apply可以接受类数组对象,恰巧就是querySelectorAll方法所返回的内容。如果我们不需要指定方法内this的值传null0即可。返回的结果即包含所有数组方法的DOM元素数组。

如果你正在用ES2015你可以使用展开运算符 ...

const nodelist = [...document.querySelectorAll('div')]; // 返回一个真正的数组

//之后 ..

nodelist.forEach(...);
nodelist.map(...);
nodelist.slice(...);

//等...

#07 - “use strict” 变得懒惰(原文)

2016-01-07 by @nainslie

(译者注:此片翻译较渣,欢迎指正,建议大家阅读原文或直接阅读MDN对严格模式的中文介绍

JavaScript的严格模式使开发者更容易写出“安全”的代码。

通常情况下,JavaScript允许程序员相当粗心,比如你可以引用一个从未用"var"声明的变量。或许对于一个刚入门的开发者来说这看起来很方便,但这也是变量拼写错误或者从作用域外引用变量时引发的一系列错误的原因。

程序员喜欢电脑帮我们做一些无聊的工作,喜欢它自动的检查我们工作上的错误。这就是"use strict"帮我们做的,它把我们的错误转变为了JavaScript错误。

我们把这个指令放在js文件顶端来使用它:

// 全脚本严格模式
"use strict";
var v = "Hi!  I'm a strict mode script!";

或者放在一个方法内:

function f()
{
  // 方法级严格模式
  'use strict';
  function nested() { return "And so am I!"; }
  return "Hi!  I'm a strict mode function!  " + nested();
}
function f2() { return "I'm not strict."; }

通过在JavaScript文件或方法内引入此指令,使JavaScript引擎运行在严格模式下,这直接禁止了许多大项目中不受欢迎的操作。另外,严格模式也改变了以下行为:

  • 只有被"var"声明过的变量才可以引用。
  • 试图写只读变量时将会报错
  • 只能通过"new"关键字调用构造方法
  • "this"不再隐式的指向全局变量
  • 对eval()有更严格的限制
  • 防止你使用预保留关键字命名变量

严格模式对于新项目来说是很棒的,但对于一些并没有使用它的老项目来说,引入它也是很有挑战性的。如果你把所有js文件都连接到一个大文件中的话,可能导致所有文件都运行在严格模式下,这可能也会有一些问题。

It is not a statement, but a literal expression, ignored by earlier versions of JavaScript. 严格模式的支持情况:

  • Internet Explorer from version 10.
  • Firefox from version 4.
  • Chrome from version 13.
  • Safari from version 5.1.
  • Opera from version 12.

MDN对严格模式的全面介绍

#06 - 可以接受单各参数和数组的方法

2016-01-06 by @mattfxyz(原文)

写一个方法可以接受单个参数也可以接受一个数组,而不是分开写两个方法。这和jQuery的一些方法的工作原理很像(css 可以修改任何匹配到的选择器).

你只要把任何东西连接到一个数组. Array.concat可以接受一个数组也可以接受单个参数。

function printUpperCase(words) {
  var elements = [].concat(words);
  for (var i = 0; i < elements.length; i++) {
    console.log(elements[i].toUpperCase());
  }
}

printUpperCase现在可以接受单个单词或多个单词的数组作为它的参数。

printUpperCase("cactus");
// => CACTUS
printUpperCase(["cactus", "bear", "potato"]);
// => CACTUS
//  BEAR
//  POTATO

#05 - undefinednull的区别(原文)

2016-01-05 by @loverajoel

  • undefined表示一个变量没有被声明,或者被声明了但没有被赋值

  • null是一个表示“没有值”的值

  • Javascript将未赋值的变量默认值设为undefined

  • Javascript从来不会将变量设为null。它是用来让程序员表明某个用var声明的变量时没有值的。

  • undefined不是一个有效的JSON而null

  • undefined的类型(typeof)是undefined

  • null的类型(typeof)是object. 为什么?

  • 它们都是基本来兴

  • 他们都是falsy (Boolean(undefined) // false, Boolean(null) // false)

  • 你可以这样判断一个变量是否是undefined

    typeof variable === "undefined"
    
- 你可以这样判断一个变量是否是[null](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/null)

  ```javascript
  variable === null
  • 双等号比较时它们相等,但三等号比较时不相等

    null == undefined // true
    
    null === undefined // false
    

## #04 - 排列带音节字母的字符串([原文](https://github.com/loverajoel/jstips#04---sorting-strings-with-accented-characters))

> 2016-01-04 by [@loverajoel](https://twitter.com/loverajoel)

Javascript有一个原生方法**[sort](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)**可以排列数组。一次简单的`array.sort()`将每一个数组元素视为字符串并按照字母表排列。你也可以提供[自定义排列方法](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Parameters) function.

```javascript
['Shanghai', 'New York', 'Mumbai', 'Buenos Aires'].sort();
// ["Buenos Aires", "Mumbai", "New York", "Shanghai"]

但是当你试图整理一个如['é', 'a', 'ú', 'c']这样的非ASCII元素的数组时,你可能会得到一个奇怪的结果['c', 'e', 'á', 'ú']。这是因为排序方法只在英文下有用。

看一下下一个例子:

// 西班牙语
['único','árbol', 'cosas', 'fútbol'].sort();
// ["cosas", "fútbol", "árbol", "único"] // bad order

// 德语
['Woche', 'wöchentlich', 'wäre', 'Wann'].sort();
// ["Wann", "Woche", "wäre", "wöchentlich"] // bad order

幸运的是,有两种方法可以解决这个问题,由ECMAScript国际化API提供的localeCompareIntl.Collator

两个方法都有自定义配置参数可以使其更好用。

使用localeCompare()

['único','árbol', 'cosas', 'fútbol'].sort(function (a, b) {
  return a.localeCompare(b);
});
// ["árbol", "cosas", "fútbol", "único"]

['Woche', 'wöchentlich', 'wäre', 'Wann'].sort(function (a, b) {
  return a.localeCompare(b);
});
// ["Wann", "wäre", "Woche", "wöchentlich"]

使用Intl.Collator()

['único','árbol', 'cosas', 'fútbol'].sort(Intl.Collator().compare);
// ["árbol", "cosas", "fútbol", "único"]

['Woche', 'wöchentlich', 'wäre', 'Wann'].sort(Intl.Collator().compare);
// ["Wann", "wäre", "Woche", "wöchentlich"]
  • 两个方法都可以自定义区域位置。For each method you can customize the location.
  • 根据Firefox,当比较大数量的字符串是Intl.Collator更快。

所以当你处理一个由非英语组成的字符串数组时,记得使用此方法来避免排序出现意外。

#03 - 优化嵌套的条件语句(原文)

2016-01-03 by AlbertoFuente

我们怎样来提高和优化javascript里嵌套的if语句呢?

if (color) {
  if (color === 'black') {
    printBlackBackground();
  } else if (color === 'red') {
    printRedBackground();
  } else if (color === 'blue') {
    printBlueBackground();
  } else if (color === 'green') {
    printGreenBackground();
  } else {
    printYellowBackground();
  }
}

一种方法来提高嵌套的if语句是用switch语句。虽然它不那么啰嗦而且排列整齐,但是并不建议使用它,因为这对于调试错误很困难。这告诉你为什么.

switch(color) {
  case 'black':
    printBlackBackground();
    break;
  case 'red':
    printRedBackground();
    break;
  case 'blue':
    printBlueBackground();
    break;
  case 'green':
    printGreenBackground();
    break;
  default:
    printYellowBackground();
}

但是如果在每个语句中都有很多条件检查时该怎么办呢?这种情况下,如果我们想要不罗嗦又整洁的话,我们可以用有条件的switch。如果我们传递trueswitch语句,我没变可以在每个case中使用条件语句了。

switch(true) {
  case (typeof color === 'string' && color === 'black'):
    printBlackBackground();
    break;
  case (typeof color === 'string' && color === 'red'):
    printRedBackground();
    break;
  case (typeof color === 'string' && color === 'blue'):
    printBlueBackground();
    break;
  case (typeof color === 'string' && color === 'green'):
    printGreenBackground();
    break;
  case (typeof color === 'string' && color === 'yellow'):
    printYellowBackground();
    break;
}

但是我们应该时刻注意避免太多判断在一个条件里,尽量少的使用switch,考虑最有效率的方法:借助object

var colorObj = {
  'black': printBlackBackground,
  'red': printRedBackground,
  'blue': printBlueBackground,
  'green': printGreenBackground,
  'yellow': printYellowBackground
};

if (color in colorObj) {
  colorObj[color]();
}

这里有更多相关的内容.

#02 - ReactJs - Keys in children components are important(原文)

(译者注:本人对ReactJs了解不多,就不翻译了,免得误导大家)

2016-01-02 by @loverajoel

The key is an attribute that you must pass to all components created dynamically from an array. It’s unique and constant id that React use for identify each component in the DOM and know that it’s a different component and not the same one. Using keys will ensure that the child component is preserved and not recreated and prevent that weird things happens.

Key is not really about performance, it’s more about identity (which in turn leads to better performance). randomly assigned and changing values are not identity Paul O’Shannessy

  • Use an existing unique value of the object.

  • Define the keys in the parent components, not in child components

    //bad
    ...
    render() {
    	<div key={{item.key}}>{{item.name}}</div>
    }
    ...
    
    //good
    <MyComponent key={{item.key}}/>
    
  • Using array index is a bad practice.

  • random() will not work

    //bad
    <MyComponent key={{Math.random()}}/>
    
  • You can create your own unique id, be sure that the method be fast and attach it to your object.

  • When the amount of child are big or involve expensive components, use keys has performance improvements.

  • You must provide the key attribute for all children of ReactCSSTransitionGroup.

#1 - AngularJs: $digest vs $apply(原文)

(译者注:本人对AngularJs了解不多,就不翻译了,免得误导大家)

2016-01-01 by @loverajoel

One of the most appreciated features of AngularJs is the two way data binding. In order to make this work AngularJs evaluates the changes between the model and the view through cycles($digest). You need to understand this concept in order to understand how the framework works under the hood.

Angular evaluates each watcher whenever one event is fired, this is the known $digest cycle. Sometimes you have to force to run a new cycle manually and you must choose the correct option because this phase is one of the most influential in terms of performance.

$apply

This core method lets you to start the digestion cycle explicitly, that means that all watchers are checked, the entire application starts the $digest loop. Internally after execute an optional function parameter, call internally to $rootScope.$digest();.

$digest

In this case the $digest method starts the $digest cycle for the current scope and its children. You should notice that the parents scopes will not be checked and not be affected.

Recommendations

  • Use $apply or $digest only when browser DOM events have triggered outside of AngularJS.

  • Pass a function expression to $apply, this have a error handling mechanism and allow integrate changes in the digest cycle

    $scope.$apply(() => {
    	$scope.tip = 'Javascript Tip';
    });
    
  • If only needs update the current scope or its children use $digest, and prevent a new digest cycle for the whole application. The performance benefit it’s self evident

  • $apply() is hard process for the machine and can lead to performance issues when having a lot of binding.

  • If you are using >AngularJS 1.2.X, use $evalAsync is a core method that will evaluate the expression during the current cycle or the next. This can improve your application’s performance.

#0 - 向数组中插入元素(原文)

2015-12-29

向一个数组中插入元素是平时很常见的一件事情。你可以使用push在数组尾部插入元素,可以用unshift在数组头部插入元素,也可以用splice在数组中间插入元素。

但是这些已知的方法,并不意味着没有更加高效的方法。让我们接着往下看……

向数组结尾添加元素用push()很简单,但下面有一个更高效的方法

var arr = [1,2,3,4,5];

arr.push(6);
arr[arr.length] = 6; // 在Mac OS X 10.11.1下的Chrome 47.0.2526.106中快43%

两种方法都是修改原始数组。不信?看看jsperf

现在我们试着向数组的头部添加元素:

var arr = [1,2,3,4,5];

arr.unshift(0);
[0].concat(arr); // 在Mac OS X 10.11.1下的Chrome 47.0.2526.106中快98%

这里有一些小区别,unshift操作的是原始数组,concat返回一个新数组,参考jsperf

使用splice可以简单的向数组总监添加元素,这也是最高效的方法。

var items = ['one', 'two', 'three', 'four'];
items.splice(items.length / 2, 0, 'hello');

我在许多浏览器和系统中进行了测试,结果都是相似的。希望这条小知识可以帮到你,也欢迎大家自行测试。

30 回复

8# 好精髓,特地来支持!

好文,收藏!

666666 很不错~~~

18#不赞成。不会为了这个性能牺牲代码可读性

@hinson0 确实是这样,所以作者也在最后说

Although ~~ may perform better, for the sake of readability please use Math.floor().

不错,mark

Mark

来自酷炫的 CNodeMD

@fundon 果然蔡神做的更专业,我已经向你的repo提了PR。

untitled1.png

第0条,看chrome 50的测试效果

转换为数字的更快方法:不是Number('str')吗?

mark,赞一个;#20我运行了一下,不太明白为什么少了第三个函数调用的打印?

mark,谢谢楼主

Sent from CNodejs for iOS

nice,干货满满的啊

回到顶部