JavaScript 优雅的实现方式包含你可能不知道的知识点

有些东西很好用,可是你未必知道;有些东西你可能用过,可是你未必知道原理。javascript

实现一个目的有多种途径,俗话说,条条大路通罗马。不少内容来自平时的一些收集以及过往博客文章底下的精彩评论,收集整理拓展一波,发散一下你们的思惟以及拓展一下知识面。前端

茴字有四种写法,233333..., 文末有彩蛋有惊喜。java

一、简短优雅地实现 sleep 函数

不少语言都有 sleep 函数,显然 js 没有,那么如何能简短优雅地实现这个方法?node

1.1 普通版

function sleep(sleepTime) {
    for(var start = +new Date; +new Date - start <= sleepTime;) {}
}
var t1 = +new Date()
sleep(3000)
var t2 = +new Date()
console.log(t2 - t1)

优势:简单粗暴,通俗易懂。
缺点:这是最简单粗暴的实现,确实 sleep 了,也确实卡死了,CPU 会飙升,不管你的服务器 CPU 有多么 Niubility。git

1.2 Promise 版本

function sleep(time) {
  return new Promise(resolve => setTimeout(resolve, time))
}

const t1 = +new Date()
sleep(3000).then(() => {
  const t2 = +new Date()
  console.log(t2 - t1)
})

优势:这种方式其实是用了 setTimeout,没有造成进程阻塞,不会形成性能和负载问题。
缺点:虽然不像 callback 套那么多层,但仍不怎么美观,并且当咱们须要在某过程当中须要中止执行(或者在中途返回了错误的值),还必须得层层判断后跳出,很是麻烦,并且这种异步并非那么完全,仍是看起来别扭。github

1.3 Generator 版本

function sleep(delay) {
  return function(cb) {
    setTimeout(cb.bind(this), delay)
  };
}

function* genSleep() {
  const t1 = +new Date()
  yield sleep(3000)
  const t2 = +new Date()
  console.log(t2 - t1)
}

async(genSleep)

function async(gen) {
  const iter = gen()
  function nextStep(it) {
    if (it.done) return
    if (typeof it.value === "function") {
      it.value(function(ret) {
        nextStep(iter.next(ret))
      })
    } else {
      nextStep(iter.next(it.value))
    }
  }
  nextStep(iter.next())
}

优势:同 Promise 优势,另外代码就变得很是简单干净,没有 then 那么生硬和恶心。
缺点:但不足也很明显,就是每次都要执行 next() 显得很麻烦,虽然有 co(第三方包)能够解决,但就多包了一层,很差看,错误也必须按 co 的逻辑来处理,不爽。正则表达式

co 之因此这么火并非没有缘由的,固然不是仅仅实现 sleep 这么无聊的事情,而是它活生生的借着generator/yield 实现了很相似 async/await 的效果!这一点真是让我三观尽毁另眼相看。chrome

const co = require("co")
function sleep(delay) {
  return function(cb) {
    setTimeout(cb.bind(this), delay)
  }
}

co(function*() {
  const t1 = +new Date()
  yield sleep(3000)
  const t2 = +new Date()
  console.log(t2 - t1)
})

1.4 Async/Await 版本

function sleep(delay) {
  return new Promise(reslove => {
    setTimeout(reslove, delay)
  })
}

!async function test() {
  const t1 = +new Date()
  await sleep(3000)
  const t2 = +new Date()
  console.log(t2 - t1)
}()

优势:同 Promise 和 Generator 优势。 Async/Await 能够看作是 Generator 的语法糖,Async 和 Await 相较于 * 和 yield 更加语义,另外各个函数都是扁平的,不会产生多余的嵌套,代码更加清爽易读。
缺点: ES7 语法存在兼容性问题,有 babel 一切兼容性都不是问题编程

至于 Async/AwaitPromiseGenerator 的好处能够参考这两篇文章:
Async/Await 比 Generator 的四个改进点
关于Async/Await替代Promise的6个理由数组

1.5 不要忘了开源的力量

在 javascript 优雅的写 sleep 等于如何优雅的不优雅,2333

这里有 C++ 实现的模块: https://github.com/ErikDubbel...
const sleep = require("sleep")

const t1 = +new Date()
sleep.msleep(3000)
const t2 = +new Date()
console.log(t2 - t1)

优势:可以实现更加精细的时间精确度,并且看起来就是真的 sleep 函数,清晰直白。
缺点:缺点须要安装这个模块,^_^,这也许算不上什么缺点。

从一个间简简单单的 sleep 函数咱们就就能够管中窥豹,看见 JavaScript 近几年来不断快速的发展,不仅仅是异步编程这一块,还有各类各样的新技术和新框架,见证了 JavaScript 的繁荣。

你可能不知道的前端知识点:Async/Await是目前前端异步书写最优雅的一种方式

二、获取时间戳

上面第一个用多种方式实现 sleep 函数,咱们能够发现代码有 +new Date()获取时间戳的用法,这只是其中的一种,下面就说一下其余两种以及 +new Date()的原理。

2.1 普通版

var timestamp=new Date().getTime()

优势:具备广泛性,你们都用这个
缺点:目前没有发现

2.2 进阶版

var timestamp = (new Date()).valueOf()
valueOf 方法返回对象的原始值(Primitive,'Null','Undefined','String','Boolean','Number'五种基本数据类型之一),多是字符串、数值或 bool 值等,看具体的对象。

优势:说明开发者原始值有一个具体的认知,让人眼前一亮。
缺点: 目前没有发现

2.3 终极版

var timestamp = +new Date()

优势:对 JavaScript 隐式转换掌握的比较牢固的一个表现
缺点:目前没有发现

如今就简单分析一下为何 +new Date() 拿到的是时间戳。

一言以蔽之,这是隐式转换的玄学,实质仍是调用了 valueOf() 的方法。

咱们先看看 ECMAScript 规范对一元运算符的规范:

一元+ 运算符

一元 + 运算符将其操做数转换为 Number 类型并反转其正负。注意负的 +0 产生 -0,负的 -0 产生 +0。产生式 UnaryExpression : - UnaryExpression 按照下面的过程执行。

  1. 令 expr 为解释执行 UnaryExpression 的结果 .
  2. 令 oldValue 为 ToNumber(GetValue(expr)).
  3. 若是 oldValue is NaN ,return NaN.
  4. 返回 oldValue 取负(即,算出一个数字相同可是符号相反的值)的结果。

+new Date() 至关于 ToNumber(new Date())

咱们再来看看 ECMAScript 规范对 ToNumber 的定义:

咱们知道 new Date() 是个对象,知足上面的 ToPrimitive(),因此进而成了 ToPrimitive(new Date())

接着咱们再来看看 ECMAScript 规范对 ToPrimitive 的定义,一层一层来,抽丝剥茧。

这个 ToPrimitive 刚开始可能不太好懂,我来大体解释一下吧:

ToPrimitive(obj,preferredType)

JavaScript 引擎内部转换为原始值 ToPrimitive(obj,preferredType) 函数接受两个参数,第一个 obj 为被转换的对象,第二个preferredType 为但愿转换成的类型(默认为空,接受的值为 NumberString

在执行 ToPrimitive(obj,preferredType) 时若是第二个参数为空而且 objDate 的实例时,此时 preferredType 会被设置为 String,其余状况下 preferredType 都会被设置为Number 若是 preferredTypeNumberToPrimitive 执行过程以下:

  1. 若是obj为原始值,直接返回;
  2. 不然调用 obj.valueOf(),若是执行结果是原始值,返回之;
  3. 不然调用 obj.toString(),若是执行结果是原始值,返回之;
  4. 不然抛异常。

若是 preferredTypeString,将上面的第2步和第3步调换,即:

  1. 若是obj为原始值,直接返回;
  2. 不然调用 obj.toString(),若是执行结果是原始值,返回之;
  3. 不然调用 obj.valueOf(),若是执行结果是原始值,返回之;
  4. 不然抛异常。

首先咱们要明白 obj.valueOf()obj.toString() 还有原始值分别是什么意思,这是弄懂上面描述的前提之一:

toString 用来返回对象的字符串表示。

var obj = {};
console.log(obj.toString());//[object Object]

var arr2 = [];
console.log(arr2.toString());//""空字符串

var date = new Date();
console.log(date.toString());//Sun Feb 28 2016 13:40:36 GMT+0800 (中国标准时间)

valueOf 方法返回对象的原始值,多是字符串、数值或 bool 值等,看具体的对象。

var obj = {
  name: "obj"
}
console.log(obj.valueOf()) //Object {name: "obj"}

var arr1 = [1]
console.log(arr1.valueOf()) //[1]

var date = new Date()
console.log(date.valueOf())//1456638436303
如代码所示,三个不一样的对象实例调用valueOf返回不一样的数据

原始值指的是 'Null','Undefined','String','Boolean','Number','Symbol' 6种基本数据类型之一,上面已经提到过这个概念,这里再次申明一下。

最后分解一下其中的过程:+new Date():

  1. 运算符 new 的优先级高于一元运算符 +,因此过程能够分解为:

var time=new Date();
+time

  1. 根据上面提到的规则至关于:ToNumber(time)
  2. time 是个日期对象,根据 ToNumber 的转换规则,因此至关于:ToNumber(ToPrimitive(time))
  3. 根据 ToPrimitive 的转换规则:ToNumber(time.valueOf())time.valueOf() 就是 原始值 获得的是个时间戳,假设 time.valueOf()=1503479124652
  4. 因此 ToNumber(1503479124652) 返回值是 1503479124652 这个数字。
  5. 分析完毕

你可能不知道的前端知识点:隐式转换的妙用

三、数组去重

注:暂不考虑对象字面量,函数等引用类型的去重,也不考虑 NaN, undefined, null等特殊类型状况。

数组样本:[1, 1, '1', '2', 1]
3.1 普通版

无需思考,咱们能够获得 O(n^2) 复杂度的解法。定义一个变量数组 res 保存结果,遍历须要去重的数组,若是该元素已经存在在 res 中了,则说明是重复的元素,若是没有,则放入 res 中。

var a = [1, 1, '1', '2', 1]
function unique(arr) {
    var res = []
    for (var i = 0, len = arr.length; i < len; i++) {
        var item = arr[i]
        for (var j = 0, len = res.length; j < jlen; j++) {
            if (item === res[j]) //arr数组的item在res已经存在,就跳出循环
                break
        }
        if (j === jlen) //循环完毕,arr数组的item在res找不到,就push到res数组中
            res.push(item)
    }
    return res
}
console.log(unique(a)) // [1, 2, "1"]

优势: 没有任何兼容性问题,通俗易懂,没有任何理解成本
缺点: 看起来比较臃肿比较繁琐,时间复杂度比较高O(n^2)

3.2 进阶版

var a =  [1, 1, '1', '2', 1]
function unique(arr) {
    return arr.filter(function(ele,index,array){
        return array.indexOf(ele) === index//很巧妙,这样筛选一对一的,过滤掉重复的
    })
}
console.log(unique(a)) // [1, 2, "1"]

优势:很简洁,思惟也比较巧妙,直观易懂。
缺点:不支持 IE9 如下的浏览器,时间复杂度仍是O(n^2)

3.3 时间复杂度为O(n)

var a =  [1, 1, '1', '2', 1]
function unique(arr) {
    var obj = {}
    return arr.filter(function(item, index, array){
        return obj.hasOwnProperty(typeof item + item) ? 
        false : 
        (obj[typeof item + item] = true)
    })
}

console.log(unique(a)) // [1, 2, "1"]

优势:hasOwnProperty 是对象的属性(名称)存在性检查方法。对象的属性能够基于 Hash 表实现,所以对属性进行访问的时间复杂度能够达到O(1);
filter 是数组迭代的方法,内部仍是一个 for 循环,因此时间复杂度是 O(n)
缺点:不兼容 IE9 如下浏览器,其实也好解决,把 filter 方法用 for 循环代替或者本身模拟一个 filter 方法。

3.4 终极版

以 Set 为例,ES6 提供了新的数据结构 Set。它相似于数组,可是成员的值都是惟一的,没有重复的值。
const unique = a => [...new Set(a)]

优势:ES6 语法,简洁高效,咱们能够看到,去重方法从原始的 14 行代码到 ES61 行代码,其实也说明了 JavaScript 这门语言在不停的进步,相信之后的开发也会愈来愈高效。
缺点:兼容性问题,现代浏览器才支持,有 babel 这些都不是问题。

你可能不知道的前端知识点:ES6 新的数据结构 Set 去重

四、数字格式化 1234567890 --> 1,234,567,890

4.1 普通版

function formatNumber(str) {
  let arr = [],
    count = str.length

  while (count >= 3) {
    arr.unshift(str.slice(count - 3, count))
    count -= 3
  }

  // 若是是否是3的倍数就另外追加到上去
  str.length % 3 && arr.unshift(str.slice(0, str.length % 3))

  return arr.toString()

}
console.log(formatNumber("1234567890")) // 1,234,567,890

优势:自我感受比网上写的一堆 for循环 还有 if-else 判断的逻辑更加清晰直白。
缺点:太普通

4.2 进阶版

function formatNumber(str) {

  // ["0", "9", "8", "7", "6", "5", "4", "3", "2", "1"]
  return str.split("").reverse().reduce((prev, next, index) => {
    return ((index % 3) ? next : (next + ',')) + prev
  })
}

console.log(formatNumber("1234567890")) // 1,234,567,890

优势:把 JS 的 API 玩的了如指掌
缺点:可能没那么好懂,不过读懂以后就会发出我怎么没想到的感受

4.3 正则版

function formatNumber(str) {
  return str.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}

console.log(formatNumber("123456789")) // 1,234,567,890

下面简单分析下正则/\B(?=(\d{3})+(?!\d))/g

  1. /\B(?=(\d{3})+(?!\d))/g:正则匹配边界\B,边界后面必须跟着(\d{3})+(?!\d);
  2. (\d{3})+:必须是1个或多个的3个连续数字;
  3. (?!\d):第2步中的3个数字不容许后面跟着数字;
  4. (\d{3})+(?!\d):因此匹配的边界后面必须跟着3*n(n>=1)的数字。

最终把匹配到的全部边界换成,便可达成目标。

优势:代码少,浓缩的就是精华
缺点:须要对正则表达式的位置匹配有一个较深的认识,门槛大一点

4.4 API版

(123456789).toLocaleString('en-US')  // 1,234,567,890

如图,你可能还不知道 JavaScripttoLocaleString 还能够这么玩。

还可使用 Intl对象 - MDN

Intl 对象是 ECMAScript 国际化 API 的一个命名空间,它提供了精确的字符串对比,数字格式化,日期和时间格式化。Collator,NumberFormat 和 DateTimeFormat 对象的构造函数是 Intl 对象的属性。
new Intl.NumberFormat().format(1234567890) // 1,234,567,890

优势:简单粗暴,直接调用 API
缺点:Intl兼容性不太好,不过 toLocaleString的话 IE6 都支持

你可能不知道的前端知识点:Intl对象 和 toLocaleString的方法。

五、交换两个整数

let a = 3,b = 4 变成 a = 4, b = 3

5.1 普通版

首先最常规的办法,引入一个 temp 中间变量

let a = 3,b = 4
let temp = a
a = b
b = temp
console.log(a, b)

优势:一眼能看懂就是最好的优势
缺点:硬说缺点就是引入了一个多余的变量

5.2 进阶版

在不引入中间变量的状况下也能交互两个变量

let a = 3,b = 4
a += b
b = a - b
a -= b
console.log(a, b)

优势:比起楼上那种没有引入多余的变量,比上面那一种稍微巧妙一点
缺点:固然缺点也很明显,整型数据溢出,好比说对于32位字符最大表示有符号数字是2147483647,也就是Math.pow(2,31)-1,若是是2147483645和2147483646交换就失败了。

5.3 终极版

利用一个数异或自己等于0和异或运算符合交换率。

let a = 3,b = 4
  a ^= b
  b ^= a
  a ^= b
console.log(a, b)

下面用竖式进行简单说明:(10进制化为二进制)

a = 011
(^) b = 100
则  a = 111(a ^ b的结果赋值给a,a已变成了7)
(^) b = 100
则  b = 011(b^a的结果赋给b,b已经变成了3)
(^) a = 111
则  a = 100(a^b的结果赋给a,a已经变成了4)

从上面的竖式能够清楚的看到利用异或运算实现两个值交换的基本过程。

下面从深层次剖析一下:

1.对于开始的两个赋值语句,a = a ^ b,b = b ^ a,至关于b = b ^ (a ^ b) = a ^ b ^ b,而b ^ b 显然等于0。所以b = a ^ 0,显然结果为a。

  1. 同理能够分析第三个赋值语句,a = a ^ b = (a ^ b) ^ a = b

注:

  1. ^ 即”异或“运算符。
它的意思是判断两个相应的位值是否为”异“,为”异"(值不一样)就取真(1);不然为假(0)。
  1. ^ 运算符的特色是与0异或,保持原值;与自己异或,结果为0。

优势:不存在引入中间变量,不存在整数溢出
缺点:前端对位操做这一块可能了解不深,不容易理解

5.4 究极版

熟悉 ES6 语法的人固然不会对解构陌生

var a = 3,b = 4;
[b, a] = [a, b]

其中的解构的原理,我暂时还没读过 ES6的规范,不知道具体的细则,不过咱们能够看看 babel 是本身编译的,咱们能够看出点门路。

哈哈,简单粗暴,不知道有没有按照 ES 的规范,其实能够扒一扒 v8的源码,chrome 已经实现这种解构用法。

这个例子和前面的例子编写风格有何不一样,你若是细心的话就会发现这两行代码多了一个分号,对于我这种编码不写分号的洁癖者,为何加一个分号在这里,实际上是有缘由的,这里就简单普及一下,建议你们仍是写代码写上分号

5.4 ECMAScript 自动分号;插入(做为补充,防止你们之后踩坑)

尽管 JavaScript 有 C 的代码风格,可是它不强制要求在代码中使用分号,实际上能够省略它们。

JavaScript 不是一个没有分号的语言,偏偏相反上它须要分号来就解析源代码。 所以 JavaScript 解析器在遇到因为缺乏分号致使的解析错误时,会自动在源代码中插入分号。

5.4.1例子
var foo = function() {
} // 解析错误,分号丢失
test()

自动插入分号,解析器从新解析。

var foo = function() {
}; // 没有错误,解析继续
test()
5.4.2工做原理

下面的代码没有分号,所以解析器须要本身判断须要在哪些地方插入分号。

(function(window, undefined) {
    function test(options) {
        log('testing!')

        (options.list || []).forEach(function(i) {

        })

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        )

        return
        {
            foo: function() {}
        }
    }
    window.test = test

})(window)

(function(window) {
    window.someLibrary = {}
})(window)

下面是解析器"猜想"的结果。

(function(window, undefined) {
    function test(options) {

        // 没有插入分号,两行被合并为一行
        log('testing!')(options.list || []).forEach(function(i) {

        }); // <- 插入分号

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        ); // <- 插入分号

        return; // <- 插入分号, 改变了 return 表达式的行为
        { // 做为一个代码段处理
            foo: function() {}
        }; // <- 插入分号
    }
    window.test = test; // <- 插入分号

// 两行又被合并了
})(window)(function(window) {
    window.someLibrary = {}; // <- 插入分号
})(window); //<- 插入分号

解析器显著改变了上面代码的行为,在另一些状况下也会作出错误的处理。

5.4.3 ECMAScript对自动分号插入的规则

咱们翻到7.9章节,看看其中插入分号的机制和原理,清楚只写之后就能够尽可能之后少踩坑

必须用分号终止某些 ECMAScript 语句 ( 空语句 , 变量声明语句 , 表达式语句 , do-while 语句 , continue 语句 , break 语句 , return 语句 ,throw 语句 )。这些分号老是明确的显示在源文本里。然而,为了方便起见,某些状况下这些分号能够在源文本里省略。描述这种状况会说:这种状况下给源代码的 token 流自动插入分号。



仍是比较抽象,看不太懂是否是,没关系,咱们看看实际例子,总结出几个规律就行,咱们先不看抽象的,看着头晕,看看具体的总结说明, 化抽象为具体

首先这些规则是基于两点:

  1. 以换行为基础;
  2. 解析器会尽可能将新行并入当前行,当且仅当符合ASI规则时才会将新行视为独立的语句。
5.4.3.1 ASI的规则

1. 新行并入当前行将构成非法语句,自动插入分号。

if(1 < 10) a = 1
console.log(a)
// 等价于
if(1 < 10) a = 1;
console.log(a);

2. 在continue,return,break,throw后自动插入分号

return
{a: 1}
// 等价于
return;
{a: 1};

3. ++、--后缀表达式做为新行的开始,在行首自动插入分号

a
++
c
// 等价于
a;
++c;

4. 代码块的最后一个语句会自动插入分号

function(){ a = 1 }
// 等价于
function(){ a = 1; }
5.4.3.2 No ASI的规则

1. 新行以 ( 开始

var a = 1
var b = a
(a+b).toString()
// 会被解析为以a+b为入参调用函数a,而后调用函数返回值的toString函数
var a = 1
var b =a(a+b).toString()

2. 新行以 [ 开始

var a = ['a1', 'a2']
var b = a
[0,1].slice(1)
// 会被解析先获取a[1],而后调用a[1].slice(1)。
// 因为逗号位于[]内,且不被解析为数组字面量,而被解析为运算符,而逗号运算符会先执
行左侧表达式,而后执行右侧表达式而且以右侧表达式的计算结果做为返回值
var a = ['a1', 'a2']
var b = a[0,1].slice(1)

3. 新行以 / 开始

var a = 1
var b = a
/test/.test(b)
// /会被解析为整除运算符,而不是正则表达式字面量的起始符号。浏览器中会报test前多了个.号
var a = 1
var b = a / test / .test(b)

4. 新行以 + 、 - 、 % 和 * 开始

var a = 2
var b = a
+a
// 会解析以下格式
var a = 2
var b = a + a

5. 新行以 , 或 . 开始

var a = 2
var b = a
.toString()
console.log(typeof b)
// 会解析为
var a = 2
var b = a.toString()
console.log(typeof b)

到这里咱们已经对ASI的规则有必定的了解了,另外还有同样有趣的事情,就是“空语句”。

// 三个空语句
;;;

// 只有if条件语句,语句块为空语句。
// 可实现unless条件语句的效果
if(1>2);else
  console.log('2 is greater than 1 always!');

// 只有while条件语句,循环体为空语句。
var a = 1
while(++a < 100);
5.4.4 结论

建议绝对不要省略分号,同时也提倡将花括号和相应的表达式放在一行, 对于只有一行代码的 if 或者 else 表达式,也不该该省略花括号。 这些良好的编程习惯不只能够提到代码的一致性,并且能够防止解析器改变代码行为的错误处理。
关于JavaScript 语句后应该加分号么?(点我查看)咱们能够看看知乎上大牛们对着个问题的见解。

你可能不知道的前端知识点:原来 JavaScript 还有位操做以及分号的使用细则

六、将 argruments 对象(类数组)转换成数组

{0:1,1:2,2:3,length:3}这种形式的就属于类数组,就是按照数组下标排序的对象,还有一个 length 属性,有时候咱们须要这种对象能调用数组下的一个方法,这时候就须要把把类数组转化成真正的数组。

6.1 普通版

var makeArray = function(array) {
  var ret = []
  if (array != null) {
    var i = array.length
    if (i == null || typeof array === "string") ret[0] = array
    else while (i) ret[--i] = array[i];
  }
  return ret
}
makeArray({0:1,1:2,2:3,length:3}) //[1,2,3]

优势:通用版本,没有任何兼容性问题
缺点:太普通

6.2 进阶版

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

这种应该是你们用过最经常使用的方法,至于为何能够这么用,不少人估计也是只知其一;不知其二,反正我看见你们这么用我也这么用,要搞清为何里面的缘由,咱们仍是从规范和源码提及。

照着规范的流程,本身看看推演一下就明白了:
英文版15.4.4.10 Array.prototype.slice (start, end)
中文版15.4.4.10 Array.prototype.slice (start, end)
若是你想知道 JavaScriptsort 排序的机制,究竟是哪一种排序好,用的哪一种,也能够从规范看出端倪。

在官方的解释中,如[mdn]

The slice() method returns a shallow copy of a portion of an array into a new array object.

简单的说就是根据参数,返回数组的一部分的 copy。因此了解其内部实现才能肯定它是如何工做的。因此查看 V8 源码中的 Array.js 能够看到以下的代码:

方法 ArraySlice源码地址,第 660 行,直接添加到 Array.prototype 上的“入口”,内部通过参数、类型等等的判断处理,分支为 SparseSliceSimpleSlice 处理。

slice.call 的做用原理就是,利用 call,将 slice 的方法做用于 arrayLikeslice 的两个参数为空,slice 内部解析使得 arguments.lengt 等于0的时候 至关于处理 slice(0) : 即选择整个数组,slice 方法内部没有强制判断必须是 Array 类型,slice 返回的是新建的数组(使用循环取值)”,因此这样就实现了类数组到数组的转化,call 这个神奇的方法、slice 的处理缺一不可。

直接看 slice 怎么实现的吧。其实就是将 array-like 对象经过下标操做放进了新的 Array 里面:

// This will work for genuine arrays, array-like objects, 
    // NamedNodeMap (attributes, entities, notations),
    // NodeList (e.g., getElementsByTagName), HTMLCollection (e.g., childNodes),
    // and will not fail on other DOM objects (as do DOM elements in IE < 9)
    Array.prototype.slice = function(begin, end) {
      // IE < 9 gets unhappy with an undefined end argument
      end = (typeof end !== 'undefined') ? end : this.length;

      // For native Array objects, we use the native slice function
      if (Object.prototype.toString.call(this) === '[object Array]'){
        return _slice.call(this, begin, end); 
      }

      // For array like object we handle it ourselves.
      var i, cloned = [],
        size, len = this.length;

      // Handle negative value for "begin"
      var start = begin || 0;
      start = (start >= 0) ? start : Math.max(0, len + start);

      // Handle negative value for "end"
      var upTo = (typeof end == 'number') ? Math.min(end, len) : len;
      if (end < 0) {
        upTo = len + end;
      }

      // Actual expected size of the slice
      size = upTo - start;

      if (size > 0) {
        cloned = new Array(size);
        if (this.charAt) {
          for (i = 0; i < size; i++) {
            cloned[i] = this.charAt(start + i);
          }
        } else {
          for (i = 0; i < size; i++) {
            cloned[i] = this[start + i];
          }
        }
      }

      return cloned;
    };

优势:最经常使用的版本,兼容性较强
缺点:ie 低版本,没法处理 dom 集合的 slice call 转数组。(虽然具备数值键值、length 符合ArrayLike 的定义,却报错)搜索资料获得 :由于 ie 下的 dom 对象是以 com 对象的形式实现的,js 对象与com对象不能进行转换 。

6.3 ES6 版本

使用 Array.from, 值须要对象有 length 属性, 就能够转换成数组

var arr = Array.from(arguments);

扩展运算符

var args = [...arguments];

ES6 中的扩展运算符...也能将某些数据结构转换成数组,这种数据结构必须有便利器接口。
优势:直接使用内置 API,简单易维护
缺点:兼容性,使用 babel 的 profill 转化可能使代码变多,文件包变大

你可能不知道的前端知识点:slice 方法的具体原理

七、数字取整 2.33333 => 2

7.1 普通版

const a = parseInt(2.33333)

parseInt() 函数解析一个字符串参数,并返回一个指定基数的整数 (数学系统的基础)。这个估计是直接取整最经常使用的方法了。
更多关于 parseInt() 函数能够查看 MDN 文档

7.2 进阶版

const a = Math.trunc(2.33333)

Math.trunc() 方法会将数字的小数部分去掉,只保留整数部分。
特别要注意的是:Internet Explorer 不支持这个方法,不过写个 Polyfill 也很简单:

Math.trunc = Math.trunc || function(x) {
  if (isNaN(x)) {
    return NaN;
  }
  if (x > 0) {
    return Math.floor(x);
  }
  return Math.ceil(x);
};

数学的事情仍是用数学方法来处理比较好。

7.3 黑科技版

7.3.1 ~~number

双波浪线 ~~ 操做符也被称为“双按位非”操做符。你一般可使用它做为代替 Math.trunc() 的更快的方法。

console.log(~~47.11)  // -> 47
console.log(~~1.9999) // -> 1
console.log(~~3)      // -> 3
console.log(~~[])     // -> 0
console.log(~~NaN)    // -> 0
console.log(~~null)   // -> 0

失败时返回0,这可能在解决 Math.trunc() 转换错误返回 NaN 时是一个很好的替代。
可是当数字范围超出 ±2^31−1 即:2147483647 时,异常就出现了:

// 异常状况
console.log(~~2147493647.123) // -> -2147473649 ?
7.3.2 number | 0

| (按位或) 对每一对比特位执行或(OR)操做。

console.log(20.15|0);          // -> 20
console.log((-20.15)|0);       // -> -20
console.log(3000000000.15|0);  // -> -1294967296 ?
7.3.3 number ^ 0

^ (按位异或),对每一对比特位执行异或(XOR)操做。

console.log(20.15^0);          // -> 20
console.log((-20.15)^0);       // -> -20
console.log(3000000000.15^0);  // -> -1294967296 ?
7.3.4 number << 0

<< (左移) 操做符会将第一个操做数向左移动指定的位数。向左被移出的位被丢弃,右侧用 0 补充。

console.log(20.15 < < 0);     // -> 20
console.log((-20.15) < < 0);  //-20
console.log(3000000000.15 << 0);  // -> -1294967296 ?

上面这些按位运算符方法执行很快,当你执行数百万这样的操做很是适用,速度明显优于其余方法。可是代码的可读性比较差。还有一个特别要注意的地方,处理比较大的数字时(当数字范围超出 ±2^31−1 即:2147483647),会有一些异常状况。使用的时候明确的检查输入数值的范围。

八、数组求和

8.1 普通版

let arr = [1, 2, 3, 4, 5]
function sum(arr){
    let x = 0
    for(let i = 0; i < arr.length; i++){
        x += arr[i]
    }
    return x
}
sum(arr) // 15

优势:通俗易懂,简单粗暴
缺点:没有亮点,太通俗

8.2 优雅版

let arr = [1, 2, 3, 4, 5]
function sum(arr) {
return arr.reduce((a, b) => a + b)
}
sum(arr) //15

优势:简单明了,数组迭代器方式清晰直观
缺点:不兼容 IE 9如下浏览器

8.3 终极版

let arr = [1, 2, 3, 4, 5]
function sum(arr) {
return eval(arr.join("+"))
}
sum(arr) //15

优势:让人一时看不懂的就是"好方法"。
缺点:

eval 不容易调试。用 chromeDev 等调试工具没法打断点调试,因此麻烦的东西也是不推荐使用的…

性能问题,在旧的浏览器中若是你使用了eval,性能会降低10倍。在现代浏览器中有两种编译模式:fast path和slow path。fast path是编译那些稳定和可预测(stable and predictable)的代码。而明显的,eval 不可预测,因此将会使用 slow path ,因此会慢。

更多关于 eval 的探讨能够关注这篇文章: JavaScript 为何不推荐使用 eval?

你可能不知道的前端知识点:eval的使用细则

最后

祝你们圣诞快乐?,欢迎补充和交流。


相关文章
相关标签/搜索