GithubHelp home page GithubHelp logo

js-china / daily-interview-tips Goto Github PK

View Code? Open in Web Editor NEW
13.0 2.0 3.0 19 KB

✍️每天花5分钟的时间,弄懂一道面试题 or Js小知识 来鞭策自己学习思考,每天进步一点,流年笑掷,未来(BAT or ATM)可期。

Home Page: https://www.javascriptc.com/

frontend-interview frontend interview javascript interview-questions reactjs vuejs html css angularjs

daily-interview-tips's People

Contributors

meibin08 avatar susouth avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

daily-interview-tips's Issues

检查某对象是否有某属性

📚在线阅读:检查某对象是否有某属性 - No.10

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

var myObject = {
  name: '@interview-questions'
};


if (myObject.name) { ... }

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

看一下较大的区别

var myObject = {
  name: '@interview-questions'
};

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 = '@interview-questions';
};
myFunc.prototype.age = '10 days';

var user = new myFunc();

user.hasOwnProperty('name'); // true

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

在线示例!

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

扩展阅读:

简单获取unix时间戳

📚在线阅读:简单获取unix时间戳 - No.49

我们经常需要使用unix时间戳计算。有很多方法可以取得unix时间戳。目前取得unix时间戳最简单最快的方法是:

const dateTime = Date.now();
const timestamp = Math.floor(dateTime / 1000);

const dateTime = new Date().getTime();
const timestamp = Math.floor(dateTime / 1000);

要取得一个具体时间的unix时间戳,将yyyy-mm-ddYYYY-MM-DDT00:00:00Z作为参数传递给Date构造函数。例如

const dateTime = new Date('2012-06-08').getTime();
const timestamp = Math.floor(dateTime / 1000);

你还可以像下面一样,在声明Date对象的时候添加一个+

const dateTime = +new Date();
const timestamp = Math.floor(dateTime / 1000);

或者对于具体时间

const dateTime = +new Date('2012-06-08');
const timestamp = Math.floor(dateTime / 1000);

在底层,运行时调用了Date对象的valueOf方法。然后一元操作符+调用了之前返回值的toNumber()方法。想要了解更多内容请参考下面链接

JS 怎样`reduce()`数组

📚在线阅读:JS 怎样reduce()数组 - No.48

文档里说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里运行

扩展阅读:

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

📚在线阅读:优化嵌套的条件语句

我们怎样来提高和优化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();
}

如果可以重构的话,我们可以试着简化函数。比如不需要为每个颜色写一个函数,而是将颜色作为函数的参数。

function printBackground(color) {
  if (!color || typeof color !== 'string') {
    return; // Invalid color, return immediately
  }
}

但是如果不能重构的话,我们必须避免过多的条件检查,避免过多使用switch。我们必须考虑最有效率的方法,使用object

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]();
}

这里有更多相关的内容.

扩展阅读:

javascript 快速(但危险)的取整方法

📚在线阅读:javascript 快速(但危险)的取整方法 - No.18

本条小知识关于性能...

你曾遇到过双波浪线~~操作符吗?它也被称为“双按位非”操作符。你通常可以使用它作为代替Math.trunc()的更快的方法。为什么呢?

一个按位非操作符~首先将输入input截取为32位,然后将其转换为-(input+1)。因此双按位非操作符将输入转换为-(-(input + 1)+1),使其成为一个趋向于0取整的好工具。对于数字的输入,它很像Math.trunc()。失败时返回0,这可能在解决Math.trunc()转换错误返回NaN时是一个很好的替代。

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

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

然而, 尽管~~可能有更好的性能,有经验的程序员通常坚持使用Math.trunc()。要明白为什么,这里有一个关于此操作符的冷静分析。

适用的情况

当CPU资源很珍贵时

~~可能在各平台上都比Math.trunc()快,但是你应该在你所关心的所有平台上测试这种猜想。同样,你通常需要执行数百万这样的操作来看看在运行时有没有明显的影响。

当不需要关心代码清晰度时

如果你想迷惑其他人,或者想在minifier/uglifier时取得更大功效,这是一种相对廉价的方式。

禁用的情况

当你的代码需要维护时

代码可读性始终是最重要的。无论你工作在一个团队,或是贡献给开源仓库,或是单飞。正如名言所说

Always code as if the person who ends up maintaining your code is a violent psychopath who knows where you live.(写代码时,要始终认为一个有暴力倾向并知道你住在哪里的人会最终维护你的代码。)

For a solo programmer, that psychopath is inevitably "you in six months".(这句不会翻译……)

当你忘记~~永远趋向于0时

新手程序员或许更关注~~的聪明之处,却忘记了“只去掉小数部分”的意义。这在将浮点数转换为数组索引或关联有序的值时很容易导致差一错误 ,这时明显需要一个不同的取整方法。 (代码可读性不高往往会导致此问题)

打个比方,如果你想得到离一个数“最近的整数”,你应该用Math.round()而不是~~,但是由于程序员的惰性和**每次使用需要敲10个键**的事实,人类的手指往往会战胜冷冷的逻辑,导致错误的结果。

相比之下,Math.xyz()(举例)函数的名字清楚的传达了它们的作用,减少了可能出现的意外的错误。

当处理大数时

因为~首先将数组转换为32位,~~的结果伪值在 ±2.15*10^12左右。如果你没有明确的检查输入值的范围,当转换的值最终与原始值有很大差距时,用户就可能触发未知的行为:

a = 2147483647.123  // 比32位最大正数,再多一点
console.log(~~a)    // ->  2147483647     (ok)
a += 10000          // ->  2147493647.123 (ok)
console.log(~~a)    // -> -2147483648     (huh?)

一个特别容易中招的地方是在处理Unix时间戳时(从1970年1月1日 00:00:00 UTC开始以秒测量)。一个快速获取的方法:

epoch_int = ~~(+new Date() / 1000)  // Date() 以毫秒计量,所以我们缩小它

然而,当处理2038年1月19日 03:14:07 UTC 之后的时间戳时(有时称为Y2038 limit), 可怕的事情发生了:

// 2040年1月1日 00:00:00.123 UTC的时间戳
epoch = +new Date('2040-01-01') / 1000 + 0.123  // ->  2208988800.123

// 回到未来!
epoch_int = ~~epoch                                 // -> -2085978496
console.log(new Date(epoch_int * 1000))             // ->  Wed Nov 25 1903 17:31:44 UTC

// 这很搞笑,让我们来取得正确答案
epoch_flr = Math.floor(epoch)                       // ->  2208988800
console.log(new Date(epoch_flr * 1000))             // ->  Sun Jan 01 2040 00:00:00 UTC
当原始输入的数据类型不确定时

因为~~可以将任何非数字类型转换为0

console.log(~~[])   // -> 0
console.log(~~NaN)  // -> 0
console.log(~~null) // -> 0

一些程序员将其看作适当输入验证的替代品。然而,这将导致奇怪的逻辑问题,因此你不能辨别违法输入还是真正的0。因此这_并不_推荐。

当很多人认为~~X == Math.floor(X)

很多人由于很多原因错误的把"双按位非"等同于Math.floor()。如果你不能准确地使用它,最终你很有可能会滥用它。

另一些人很细心的注意正数使用Math.floor()而负数使用Math.ceil(),但这又强制你在处理它的时候需要停下来想一想你处理的数是什么值。这又违背了使用~~快捷无陷阱的目的。

结论

尽量避免,并有节制的使用。

使用

  1. 谨慎使用。
  2. 在应用前检查值。
  3. 仔细记录被转化值的相关假设。
  4. 审查代码至少处理:
    • 逻辑错误,不合法的输入作为合法的0传入其他代码模块
    • 输入转换后范围错误
    • 错误的舍入方向导致差一错误

扩展阅读:

可以接受单参数与数组的方法

📚在线阅读:可以接受单参数与数组的方法

写一个方法可以接受单个参数也可以接受一个数组,而不是分开写两个方法。这和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现在可以接受单个单词或多个单词的数组作为它的参数。同时也可以避免在不传递参数时抛出的TypeError错误的隐患。

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

扩展阅读:

JS中的短路求值

📚在线阅读:JS中的短路求值 - No.27

短路求值是说, 只有当第一个运算数的值无法确定逻辑运算的结果时,才对第二个运算数进行求值:当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.

扩展阅读:

更简单的使用indexOf实现contains功能

📚在线阅读:更简单的使用indexOf实现contains功能 - No.15

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及其更低版本并不支持
最好在受控的环境中使用此功能

扩展阅读:

清空数组的两种方法

📚在线阅读:清空数组的两种方法 - No.22

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

// 定义一个数组
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

扩展阅读:

排列含音节字母的字符串

📚在线阅读:排列含音节字母的字符串

Javascript有一个原生方法**sort**可以排列数组。一次简单的array.sort()将每一个数组元素视为字符串并按照字母表排列。你也可以提供自定义排列方法

['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"]
  • 两个方法都可以自定义区域位置。
  • 根据Firefox,当比较大数量的字符串时,使用Intl.Collator更快。

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

扩展阅读:

Array - 向数组中插入元素

📚在线阅读:向数组中插入元素 - No.0

向一个数组中插入元素

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

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

向数组结尾添加元素

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

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

arr.push(6);
arr[arr.length] = 6;
arr2 = arr.concat([6]);

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

手机上的效率

Android (v4.2.2)

  1. arr.push(6); and arr[arr.length] = 6; 性能相同 // 3 319 694 ops/sec
  2. arr2 = arr.concat([6]); 比其他两个方法慢50.61%

Chrome Mobile (v33.0.0)

  1. arr[arr.length] = 6; // 6 125 975 ops/sec
  2. arr.push(6); 慢66.74%
  3. arr2 = arr.concat([6]); 慢87.63%

Safari Mobile (v9)

  1. arr[arr.length] = 6; // 7 452 898 ops/sec
  2. arr.push(6); 慢40.19%
  3. arr2 = arr.concat([6]); 慢49.78%
最快的为

1. arr[arr.length] = 6; // 平均 5 632 856 ops/sec
2. arr.push(6); // 慢35.64%
3. arr2 = arr.concat([6]); // 慢62.67%

桌面上的效率

Chrome (v48.0.2564)

  1. arr[arr.length] = 6; // 21 602 722 ops/sec
  2. arr.push(6); 慢61.94%
  3. arr2 = arr.concat([6]); 慢87.45%

Firefox (v44)

  1. arr.push(6); // 56 032 805 ops/sec
  2. arr[arr.length] = 6; 慢0.52%
  3. arr2 = arr.concat([6]); 慢87.36%

IE (v11)

  1. arr[arr.length] = 6; // 67 197 046 ops/sec
  2. arr.push(6); 慢39.61%
  3. arr2 = arr.concat([6]); 慢93.41%

Opera (v35.0.2066.68)

  1. arr[arr.length] = 6; // 30 775 071 ops/sec
  2. arr.push(6); 慢71.60%
  3. arr2 = arr.concat([6]); 慢83.70%

Safari (v9.0.3)

  1. arr.push(6); // 42 670 978 ops/sec
  2. arr[arr.length] = 6; 慢0.80%
  3. arr2 = arr.concat([6]); 慢76.07%
最快的为

1. arr[arr.length] = 6; // 平均42 345 449 ops/sec
2. arr.push(6); // 慢34.66%
3. arr2 = arr.concat([6]); // 慢85.79%

向数组的头部添加元素

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

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

arr.unshift(0);

[0].concat(arr);

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

手机上的效率 :

Android (v4.2.2)

  1. [0].concat(arr); // 1 808 717 ops/sec
  2. arr.unshift(0); 慢97.85%

Chrome Mobile (v33.0.0)

  1. [0].concat(arr); // 1 269 498 ops/sec
  2. arr.unshift(0); 慢99.86%

Safari Mobile (v9)

  1. arr.unshift(0); // 3 250 184 ops/sec
  2. [0].concat(arr); 慢33.67%
最快的为

1. [0].concat(arr); // 平均4 972 622 ops/sec
2. arr.unshift(0); // 慢64.70%

桌面上的效率

Chrome (v48.0.2564)

  1. [0].concat(arr); // 2 656 685 ops/sec
  2. arr.unshift(0); 慢96.77%

Firefox (v44)

  1. [0].concat(arr); // 8 039 759 ops/sec
  2. arr.unshift(0); 慢99.72%

IE (v11)

  1. [0].concat(arr); // 3 604 226 ops/sec
  2. arr.unshift(0); 慢98.31%

Opera (v35.0.2066.68)

  1. [0].concat(arr); // 4 102 128 ops/sec
  2. arr.unshift(0); 慢97.44%

Safari (v9.0.3)

  1. arr.unshift(0); // 12 356 477 ops/sec
  2. [0].concat(arr); 慢15.17%
最快的为

1. [0].concat(arr); // 平均6 032 573 ops/sec
2. arr.unshift(0); // 慢78.65%

向数组中间添加元素

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

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

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

推荐阅读:

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

📚在线阅读:你知道在函数参数内也可以使用解构吗? - No.43

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

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

sayHello({ name: 'John', surname: 'Smith' })
// -> Hello John Smith! How are you?

这对于接收可选参数的函数,是很棒的。对于这种用法,你也可以添加默认参数值来填充调用者没有传递或忘记传递的参数值:

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

= {}表示此参数需要解构的默认对象是一个{},以防调用者忘记传值,或传递了一个错误类型(大多情况为后者)。

sayHello2()
// -> Hello Anony Moose! How are you?
sayHello2({ name: "Bull" })
// -> Hello Bull Moose! How are you?
参数处理

对于普通的解构,如果输入的参数与函数指定的对象参数不符,所有不符的参数都将为undefined,所以你需要增加代码来正确的处理这些情况:

var sayHelloTimes = function({ name, surname }, times) {
  console.log(`Hello ${name} ${surname}! I've seen you ${times} times before.`);
}

sayHelloTimes({ name: "Pam" }, 5678)
// -> Hello Pam undefined! I've seen you 5678 times before.
sayHelloTimes(5678)
// -> Hello undefined undefined! I've seen you undefined times before.

更糟糕的,如果没有传递需要解构的的参数,将会抛出错误,这可能使你的应用崩溃:

sayHelloTimes()
// -> Uncaught TypeError: Cannot match against 'undefined' or 'null'...

这与访问一个未定义对象的参数基本相似,只是错误类型不太一样。

为解构增加默认参数基本上解决了上面的所有问题:

var sayHelloTimes2 = function({ name = "Anony", surname = "Moose" } = {}, times) {
  console.log(`Hello ${name} ${surname}! I've seen you ${times} times before.`);
};

sayHelloTimes2({ name: "Pam" }, 5678)
// -> Hello Pam Moose! I've seen you 5678 times before.
sayHelloTimes2(5678)
// -> Hello Anony Moose! I've seen you undefined times before.
sayHelloTimes2()
// -> Hello Anony Moose! I've seen you undefined times before.

对于= {},它掩盖了_object_未传递时的情况,但对于个别属性默认值的情形下会抛出异常:

var sayHelloTimes2a = function({ name = "Anony", surname = "Moose" }, times) {
  console.log(`Hello ${name} ${surname}! I've seen you ${times} times before.`);
};

sayHelloTimes2a({ name: "Pam" }, 5678)
// -> Hello Pam Moose! I've seen you 5678 times before.
sayHelloTimes2a(5678)
// -> Hello Anony Moose! I've seen you undefined times before.
sayHelloTimes2a()
// -> Uncaught TypeError: Cannot match against 'undefined' or 'null'.
可用性

需要注意解构赋值可能在你正在使用的Node.js或浏览器中默认情况下并不可用。对于Node.js,你可以在启动时使用--harmony-destructuring标记开启此特性。

扩展阅读:

ES6中的伪强制参数

📚在线阅读:优化嵌套的条件语句 - No.12

在许多编程语言中,方法的参数是默认强制需要的,开发人员必须明确定义一个可选的参数。在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看到更多关于默认参数特性的例子。

扩展阅读:

使用立即执行函数表达式

📚在线阅读:使用立即执行函数表达式 - No.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
  3. 效率 jsPerf
  4. 带你理解 JS 容易出错的坑和细节
  5. JS 原生面经从初级到高级【近1.5W字】

如何向回调方法传递参数

📚在线阅读:如何向回调方法传递参数 - No.16

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

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.

扩展阅读:

js 实现异步循环

📚在线阅读:js 实现异步循环 - No.34

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

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

如上程序的输出为:

> 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 * (i+1));
}

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

> 4
> 4
> 4
> 4
> 4

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

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

解决方法

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

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

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

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

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

扩展阅读:

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

📚在线阅读:计算数组的平均值与中位数 - No.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

扩展阅读:

JS 变量声明

📚在线阅读:JS 变量声明 - No.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

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

你可以在这里测试并看到结果 感谢babel.

更多相关内容请看MDN.

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

扩展阅读:

Javascript安全的字符串拼接

📚在线阅读:安全的字符串拼接 - No.19

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

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"

当然现在 ES6(ECMAScript 6)出来了,事情就变得简单多了

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

var result = `${one}${two}${three}`; //"123"

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

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

扩展阅读:

Js 对数组洗牌

📚在线阅读:Js 对数组洗牌 - No.21

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

方法一:

比较简单的版本

[12,4,16,3].sort(function() {
    return 0.5 - Math.random();
});
//输出:[16, 12, 3, 4] 每次都不一样

方法二:

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]

扩展阅读:

前端人员需要了解的传值机制

📚在线阅读:前端人员需要了解的传值机制 - No.44

理论上,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

扩展阅读:

如何优秀的使用JSON.Stringify

📚在线阅读:使用JSON.Stringify - No.40

假如有一个对象具有参数"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"
}
*/

扩展阅读:

测量javascript代码块性能的小知识

📚在线阅读:测量javascript代码块性能的小知识 - No.13

快速的测量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

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

注意:由于Mozilla不建议将其使用在线上项目中,建议仅在开发中使用。

扩展阅读:

使用"use strict" 变得懒惰

📚在线阅读:使用"use strict" 变得懒惰

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

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文件都连接到一个大文件中的话,可能导致所有文件都运行在严格模式下,这可能也会有一些问题。

它不是一个声明,而是一个表达式,被低版本的JavaScript忽略。
严格模式的支持情况:

  • Internet Explorer 10+
  • Firefox 4+
  • Chrome 13+
  • Safari 5.1+
  • Opera 12+

扩展阅读:

预防unapply攻击

📚在线阅读:预防unapply攻击 - No.42

重写内置对象的原型方法,外部代码可以通过重写代码达到暴漏和修改已绑定参数的函数。这在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攻击。

扩展阅读:

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

📚在线阅读:避免修改和传递arguments给其他方法 — 影响优化 - No.31

背景

在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];
}

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

扩展阅读:

undefined与null的区别

📚在线阅读:undefined与null的区别

  • 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
  variable === null
  • 双等号比较时它们相等,但三等号比较时不相等
  null == undefined // true

  null === undefined // false

扩展阅读:

返回对象,使方法可以链式调用

📚在线阅读:返回对象,使方法可以链式调用 - No.20

在面向对象的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();

扩展阅读:

模板字符串

📚在线阅读:模板字符串

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)甚至使用逻辑运算符

var val1 = 1, val2 = 2;
console.log(`${val1} is ${val1 < val2 ? 'less than': 'greater than'} ${val2}`)
// 1 is less than 2

你也可以使用函数修改末班字符串的输出内容;这被称为带标签的模板字符串,其中包含了带标签的模板字符串的示例.

扩展阅读:

JS 赋值技巧

📚在线阅读:JS 赋值技巧 - No.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;

++-- 操作符

对于++操作符有些特殊。最好用下面的例子解释一下:

var a = 2;
var b = a++;
// 现在 a == 3  b == 2

a++做了如下工作:

  1. 返回a的值
  2. a增加1

但是如果我们想让值先增加呢?这也很容易:

var a = 2;
var b = ++a;
// 现在a和b都是3

看明白了吗?我将操作符放在了参数_前面_。

--操作符除了使值减小外,其他功能是类似的。

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'
};

扩展阅读:

简单监听DOM事件

📚在线阅读:简单而优雅地操作DOM事件的方法 - No.51

很多人还在这样做:

  • element.addEventListener('type', obj.method.bind(obj))
  • element.addEventListener('type', function (event) {})
  • element.addEventListener('type', (event) => {})

上面所有的例子都创建了一个匿名事件监控句柄,且在不需要时无法删除它。这在你不需要某句柄,而它却被用户或事件冒泡偶然触发时,可能会导致性能问题或不必要的逻辑问题。

更安全的事件处理方式如下:

使用引用:

const handler = function () {
  console.log("Tada!")
}
element.addEventListener("click", handler)
// 之后
element.removeEventListener("click", handler)

命名的函数移除它本身:

element.addEventListener('click', function click(e) {
  if (someCondition) {
    return e.currentTarget.removeEventListener('click', click);
  }
});

更好的写法:

function handleEvent (eventName, {onElement, withCallback, useCapture = false} = {}, thisArg) {
  const element = onElement || document.documentElement

  function handler (event) {
    if (typeof withCallback === 'function') {
      withCallback.call(thisArg, event)
    }
  }

  handler.destroy = function () {
    return element.removeEventListener(eventName, handler, useCapture)
  }

  element.addEventListener(eventName, handler, useCapture)
  return handler
}

// 你需要的时候
const handleClick = handleEvent('click', {
  onElement: element,
  withCallback: (event) => {
    console.log('Tada!')
  }
})

// 你想删除它的时候
handleClick.destroy()

扩展阅读:

Node.js - 运行未被引用的模块

📚在线阅读:Node.js - 运行未被引用的模块 - No.17

在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的文档

扩展阅读:

仅用一行生成`[0, 1, ..., N-1]`数列

📚在线阅读:仅用一行生成[0, 1, ..., N-1]数列 - No.33

使用下面一行代码,我们就可以生成0...(N-1)数列。

方法1 (需要 ES5)

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

简要说明

  1. Array.apply(null, {length: N}) 返回一个由undefined填充的长度为N的数组(例如 A = [undefined, undefined, ...])。
  2. A.map(Function.call, Number) 返回一个长度为N的数组,它的索引为I的元素为Function.call.call(Number, undefined, I, A)的结果。
  3. Function.call.call(Number, undefined, I, A)可转化为Number(I),正好就是I
  4. 结果为:[0, 1, ..., N-1]

方法2 (需要 ES6)

这里用到了Array.from https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/from

 Array.from(new Array(N),(val,index)=>index);

简要说明

  1. A = new Array(N) 返回一个有N个_小孔_的数组 (例如 A = [,,,...], 但是对于x in 0...N-1A[x] = undefined)。
  2. F = (val,index)=>indexfunction F (val, index) { return index; }
  3. Array.from(A, F) 返回一个长度为N的数组,它的索引为I的元素为F(A[I], I)的结果,也就是I
  4. 结果为:[0, 1, ..., N-1]

One More Thing

如果你需要[1, 2, ..., N]序列, 方法1 可改为:

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

方法2可改为:

Array.from(new Array(N),(val,index)=>index+1);

扩展阅读:

理解变量提升

📚在线阅读:理解变量提升 - No.11

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

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!');
  }
}

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

扩展阅读:

运用存储加速递归 Speed up recursive functions with memoization

📚在线阅读:运用存储加速递归 Speed up recursive functions with memoization - No.29

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

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

它可以运行,但并不高效。它做了太多重复的运算,我们可以通过存储这些运算结果来使其加速。

var fibonacci = (function() {
  var cache = [0, 1]; // cache the value at the n index
  return function(n) {
    if (cache[n] === undefined) {
      for (var i = cache.length; i <= n; ++i) {
        cache[i] = cache[i-1] + cache[i-2];
      }
    }
    return cache[n];
  }
})()

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

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);

ES6版本的memoize函数如下:

var memoize = function(func){
    const cache = {};
    return (...args) => {
        const key = [...args].toString();
        return key in cache ? cache[key] : (cache[key] = func(...args));
    }
}
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

扩展阅读:

扩展插件中观察DOM的变化

📚在线阅读:扩展插件中观察DOM的变化 - No.36

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);

})()

扩展阅读:

计算数组中的最大值/最小值

📚在线阅读:计算数组中的最大值/最小值 - No.45

内置函数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

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

扩展阅读:

Javascript多维数组扁平化

📚在线阅读:Javascript多维数组扁平化 - No.38

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

对于此数组:

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]

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

方案四:使用 ES6 的展开运算符

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

这里 查看这四种方法

对于无限嵌套的数组请使用 Lodash 的 flattenDeep()

如果你担心性能问题的话,这里 有一个测试让你确认他们是如何执行的。

对于较大的数组试一下Underscore的flatten().

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

扩展阅读:

将Node List转换为数组(Array)

📚在线阅读:将Node List转换为数组(Array)

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元素数组。

另外你可以使用Array.prototype.slice结合Function.prototype.callFunction.prototype.apply, 将类数组对象当做this传入:

const nodelist = document.querySelectorAll('div');
const nodelistToArray = Array.prototype.slice.call(nodelist); // or equivalently Array.prototype.slice.apply(nodelist);

//之后 ..

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

//等...

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

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

//之后 ..

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


//等...

扩展阅读:

实用的`log`技巧

📚在线阅读:前端人员需要了解的传值机制 - No.50

使用条件断点输出log

如果你想当函数每次被调用时都在控制台打印一个值,你可以应用条件断点来实现。打开你的开发工具,找到你准备打印的值所在的函数然后使用如下条件设置一个条件断点:

console.log(data.value) && false

条件断点只有在条件运行的结果为true时才会中断页面。所以使用console.log('foo') && false这样的条件,由于你把false放在了AND条件中,所以结果肯定是false。因此这并不会中断页面但是会打印log到控制台。这也可以应用在计算某个函数或回调被调用了多少次上面。

这里有各个平台下设置条件断点的方法:EdgeChromeFirefoxSafari

打印函数到控制台

你曾经有过打算打印函数到控制台却不能看到函数的代码的情况吗?最快的方法查看函数的代码是将其与空字符串连接,从而将其强制转换为字符串。

function funcVariable(){
  var a = "码农题库";
}
console.log(funcVariable + '');

扩展阅读:

请使用 === 而不是 ==

📚在线阅读:使用 === 而不是 == - No.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

扩展阅读:

ES6 箭头函数

📚在线阅读:ES6 箭头函数 小技巧 - No.14

介绍一个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.(译者注:一些其他语言中的箭头函数,避免不准确就不翻译了 欢迎PR)

有什么益处呢?

  • 语法: 更少的代码行; 不再需要一遍一遍的打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

扩展阅读:

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

📚在线阅读:Map()的营救;使对象属性有顺序 - No.32

对象属性顺序

一个对象是一个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 objectKeys) {
	myObject[item]
...

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

扩展阅读:

转换为数字的更快方法

📚在线阅读:转换为数字的更快方法 - No.23

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

var one = '1';

var numberOne = +one; // Number 1

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

var one = '1';

var negativeNumberOne = -one; // Number -1

parseFloat

如果你不解析 16 进制数,这是一个非常好的选择。例如:

parseInt(-0xff); // returns -255
parseInt("-0xFF"); // returns -255
parseFloat(-0xff); // returns -255
parseFloat("-0xFF"); // returns 0

按位非

可以把字符串转换成整数,但他不是浮点数。如果是一个字符串转换,它将返回 0;

~~1.23; // returns 1
~~"1.23"; // returns 1
~~"23"; // returns 23
~~"Hello world"; // returns 0

扩展阅读:

过滤并排序字符串列表

📚在线阅读:过滤并排序字符串列表 - No.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.lastIndexOf(keyword) === index;
    })
  .sort(function (a, b) {
      return a < b ? -1 : 1;
    });

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

const filteredAndSortedKeywords = keywords
  .filter((keyword, index) => keywords.lastIndexOf(keyword) === index)
  .sort((a, b) => a < b ? -1 : 1);

这是最后过滤和排序后的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']

感谢@nikshulipa@kirilloid@lesterzone@tracker1@manuel_del_pozo所有的回复与建议!

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

  1. 数组排序- JavaScript中的sort如果对字符串/对象进行排序,排序 ...
  2. js将数组中的字符串执行字母表排序
  3. 带你理解 JS 容易出错的坑和细节

JavaScript 数组去重

📚在线阅读:JavaScript 数组去重 - No.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

扩展阅读:

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

📚在线阅读:柯里化(currying)与部分应用(partial application) - No.28

柯里化(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);
  }
}

扩展阅读:

纯JS监听document是否加载完成

📚在线阅读:纯JS监听document是否加载完成 - No.46

跨浏览器且纯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是否加载完成。

扩展阅读:

Javascript高级特性

📚在线阅读:Javascript高级特性 - No.39

在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这样的辅助方法时是尤为重要的。要小心!

链接

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.