Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
jawil opened this issue Dec 23, 2017 · 44 comments
Open

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

jawil opened this issue Dec 23, 2017 · 44 comments

Comments

@jawil
Copy link
Owner

jawil commented Dec 23, 2017

有些东西很好用,但是你未必知道;有些东西你可能用过,但是你未必知道原理。
实现一个目的有多种途径,俗话说,条条大路通罗马。很多内容来自平时的一些收集以及过往博客文章底下的精彩评论,收集整理拓展一波,发散一下大家的思维以及拓展一下知识面。

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

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

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

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。

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 套那么多层,但仍不怎么美观,而且当我们需要在某过程中需要停止执行(或者在中途返回了错误的值),还必须得层层判断后跳出,非常麻烦,而且这种异步并不是那么彻底,还是看起来别扭。

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 的效果!这一点真是让我三观尽毁刮目相看。

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/ErikDubbelboer/node-sleep

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是目前前端异步书写最优雅的一种方式

2、获取时间戳

上面第一个用多种方式实现 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
  2. 根据上面提到的规则相当于:ToNumber(time)
  3. time 是个日期对象,根据 ToNumber 的转换规则,所以相当于:ToNumber(ToPrimitive(time))
  4. 根据 ToPrimitive 的转换规则:ToNumber(time.valueOf())time.valueOf() 就是 原始值 得到的是个时间戳,假设 time.valueOf()=1503479124652
  5. 所以 ToNumber(1503479124652) 返回值是 1503479124652 这个数字。
  6. 分析完毕

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

3、数组去重

注:暂不考虑对象字面量,函数等引用类型的去重,也不考虑 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 去重

4、数字格式化 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的方法。

5、交换两个整数

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。
2. 同理可以分析第三个赋值语句,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 还有位操作以及分号的使用细则

6、将 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 方法的具体原理

7、数字取整 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、数组求和

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的使用细则

最后

祝大家圣诞快乐🎄,欢迎补充和交流。


@jawil
Copy link
Owner Author

jawil commented Dec 23, 2017

圣诞快乐,1024邀请码,有缘人得!
a6e1bbfa4e2e4bf7 | 2017-12-23 17:50:04 |   |   | 未使用 | 邀请

c7a5a23f4f0e5cc7 | 2017-12-23 17:50:04 |   |   | 未使用 | 邀请

@lidongjies
Copy link

这是啥

@huyansheng3
Copy link

完全是 js 的糟粕吧。。。。各种隐式的代码背后的逻辑说不定那个版本就改了。。老老实实用官方 api 不好吗?

@jawil
Copy link
Owner Author

jawil commented Dec 23, 2017

不可能改的,JavaScript 设计者都承认 null 是个对象都是一种错误的决定,也没看改过来,原因你懂得。
为了向下兼容,这些东西都是 ECMAScript 的东西,怎么能说不定就改了,对于 JavaScript 这种脚本语言,只可能拓展新的功能,不可能改变原来的功能的。

还有就是糟粕问题,完全不敢苟同,我觉得是玄学。@huyansheng3

@axuebin
Copy link

axuebin commented Dec 23, 2017

数组去重那部分小标题权重错了。。

@jawil
Copy link
Owner Author

jawil commented Dec 23, 2017

感谢指正,哈哈,这些细节都被看出来了 @axuebin

@xinkule
Copy link

xinkule commented Dec 23, 2017

大佬真的很会玩。。。最容易懂的才是好方法呀

@LingZhenhua
Copy link

LingZhenhua commented Dec 23, 2017

1.2 Promise 版本运行报错是为何
//我又仔细看了看,没有错,在chrome里的console我直接粘贴进去没加分号,尴尬,不过直接在node.js运行的话,就没问题
`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);
})`
//如果没加分号,在chrome里的报错是 Uncaught SyntaxError: Identifier 't1' has already been declared
// at :1:1

@jawil
Copy link
Owner Author

jawil commented Dec 23, 2017

有报错截图吗?请在 Chrome 浏览器运行或者在 node 高版本环境运行@LingZhenhua

@xanke
Copy link

xanke commented Dec 24, 2017

获取时间戳还可以 Date.now()

@jawil
Copy link
Owner Author

jawil commented Dec 24, 2017

哈哈,把这个竟然忘了,不错,谢谢补充~ @xanke

@limoning
Copy link

貌似 +new Date 也一样可以获取的

@jawil
Copy link
Owner Author

jawil commented Dec 24, 2017

不带参数就是当前时间的时间戳,确实可以 @limoning

@GitHubJiKe
Copy link

这些玄学,写在代码里,懂的人,会心一笑,不懂得人,能懵一脸血!

@unix
Copy link

unix commented Dec 24, 2017

typo: '便利器接口' => '遍历器接口 (Iterator)'

@axuebin
Copy link

axuebin commented Dec 25, 2017

@jawil 还得感谢GayHub啊... 一眼就看出来了...

@GONDARJJC
Copy link

数字格式化那里可以用reduceRight,就不用reverse了。
学习到了好多新知识,感谢分享

@IMLaoJI
Copy link

IMLaoJI commented Dec 25, 2017

return obj.hasOwnProperty(typeof item + item) ?
false :
(obj[typeof item + item] = true)
这咋理解。。。

@unix
Copy link

unix commented Dec 25, 2017

@IMLaoJI
typeof item + item 就是设置一个键,键名为值的类型 + 值。
然后查找这个缓存的对象里面有没有相同的键就知道这个值是不是遍历过了。

@FrontToEnd
Copy link

4.4 API版的例子:

(123456789).toLocaleString('en-US')   //值应该是"123,456,789"

@erniu
Copy link

erniu commented Dec 25, 2017

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"]

3.1 第二个循环的长度变量手误了,23333

@IMLaoJI
Copy link

IMLaoJI commented Dec 25, 2017

@WittBulter 好棒 谢谢你!

@lynxerzhang
Copy link

想请教下,在讲述Array.slice那节里面,“因为 ie 下的 dom 对象是以 com 对象的形式实现的,js 对象与com对象不能进行转换 ”,com对象是指什么?

@jawil
Copy link
Owner Author

jawil commented Dec 26, 2017

两种不同的标准,早期浏览器战争时代,各自都执行各自的标准,那时候标准都不统一。
HTML DOM 是 W3C 标准(是 HTML 文档对象模型的英文缩写,Document Object Model for HTML)。
HTML DOM 定义了用于 HTML 的一系列标准的对象,以及访问和处理 HTML 文档的标准方法。
通过 DOM,可以访问所有的 HTML 元素,连同它们所包含的文本和属性。可以对其中的内容进行修改和删除,同时也可以创建新的元素。
HTML DOM 独立于平台和编程语言。它可被任何编程语言诸如 Java、JavaScript 和 VBScript 使用。
COM组件是遵循COM规范编写、以Win32动态链接库(DLL)或可执行文件(EXE)形式发布的可执行二进制代码,能够满足对组件架构的所有需求。遵循COM的规范标准,组件与应用、组件与组件之间可以互操作,极其方便地建立可伸缩的应用系统。COM是一种技术标准,其商业品牌则称为ActiveX。
@lynxerzhang

@scplay
Copy link

scplay commented Dec 26, 2017

�toLocalString 居然不能转成大写的中文,一点都不好玩

@lynxerzhang
Copy link

@jawil 感谢回复,谢谢!

@zoffyzhang
Copy link

后端的人也很少写位操作运算吧,好像只有搞过算法竞赛的人才喜欢写

@zhangbuding
Copy link

无聊翻着看,看到说的 xx 邀请码,直接公司大屏幕搜了下....之前真不知道1024是个啥

@IMLaoJI
Copy link

IMLaoJI commented Dec 28, 2017

@ayou33
Copy link

ayou33 commented Jan 1, 2018

谢谢分享

@xiyuyizhi
Copy link

对于数字字符串转千分位的分析有些异议

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

console.log(formatNumber("123456789")) // 1,234,567,890
  1. \B是 不是单词边界,也就是除了'1'前面的位置,1和2之间,2和3之间,3和4之间等

  2. (?=(\d{3})+(?!\d))的作用就是说明,除了1之前的位置,其他字符之间的边界,后面必须跟着3N个数字直到字符串末尾,把(?!\d)换成$更好。/\B(?=(\d{3})+$)/g

@liuqipeng417
Copy link

image
此处是否有问题,Date 实例走 ToPrimitive 优先走 toString?

@lyh2668
Copy link

lyh2668 commented Jan 22, 2018

很多都很有用的,不过对于+new Date()这块一时还不是很明白。
我也不喜欢加分号,不过写交换变量的时候的确存在问题...那就把分号加前面吧...
;[a, b] = [b, a]

@haishenming
Copy link

github还能这样用......

@hua1995116
Copy link

交换顺序那个,我还看到过
var a = 2,b =3; b = [a, a=b][0]

@Dcatfly
Copy link

Dcatfly commented Mar 20, 2018

@hua1995116 前几天好像还看到小胡子哥在知乎上写过这个?

@plh97
Copy link

plh97 commented May 2, 2018

真的是大神

@ghost
Copy link

ghost commented May 2, 2018

@xiyuyizhi 其实可以再深入一些,可以考虑带小数的情况

function formatNumber(num) {
    return String(num).replace(/\d(?=(\d{3})+(\.\d+)?$)/g, '$&,');
}

@yanche
Copy link

yanche commented May 20, 2018

看到没人提,就提一下
var args = [...arguments]; 对于arguments对象是可以的
但是对一般的 {0:"a", 1:"b", length: 2} 这种是不行的,Array.from 都可以

@hooper-hc
Copy link

reduce 有风险, 如果数组是空数组 [], 会爆错老哥

@xieww
Copy link

xieww commented Jun 28, 2018

总结挺到位。

@forzalianjunting
Copy link

@yanche 那是因为扩展运算符会默认调用 Iterator 接口,而 arguments 原生具备 Iterator 接口

@lynxerzhang
Copy link

lynxerzhang commented Aug 23, 2018

空数组导致reduce报错那个,看文档是因为

initialValue
用作第一个调用 callback的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。

所以initialValue不要空着,加上0就行了

let arr = []
function sum(arr) {
    return arr.reduce((a, b) => a + b, 0)
}
sum(arr) //0

这样就不会报错了

@zhaoleipeng
Copy link

亲嘴的那个动图咋弄出来的撒。给个调用方法。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests