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专题之跟着 underscore 学节流 #26

Open
mqyqingfeng opened this issue Jun 19, 2017 · 109 comments
Open

JavaScript专题之跟着 underscore 学节流 #26

mqyqingfeng opened this issue Jun 19, 2017 · 109 comments
Labels

Comments

@mqyqingfeng
Copy link
Owner

@mqyqingfeng mqyqingfeng commented Jun 19, 2017

前言

《JavaScript专题之跟着underscore学防抖》中,我们了解了为什么要限制事件的频繁触发,以及如何做限制:

  1. debounce 防抖
  2. throttle 节流

今天重点讲讲节流的实现。

节流

节流的原理很简单:

如果你持续触发事件,每隔一段时间,只执行一次事件。

根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。
我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。

关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。

使用时间戳

让我们来看第一种方法:使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。

看了这个表述,是不是感觉已经可以写出代码了…… 让我们来写第一版的代码:

// 第一版
function throttle(func, wait) {
    var context, args;
    var previous = 0;

    return function() {
        var now = +new Date();
        context = this;
        args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}

例子依然是用讲 debounce 中的例子,如果你要使用:

container.onmousemove = throttle(getUserAction, 1000);

效果演示如下:

使用时间戳

我们可以看到:当鼠标移入的时候,事件立刻执行,每过 1s 会执行一次,如果在 4.2s 停止触发,以后不会再执行事件。

使用定时器

接下来,我们讲讲第二种实现方式,使用定时器。

当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。

// 第二版
function throttle(func, wait) {
    var timeout;
    var previous = 0;

    return function() {
        context = this;
        args = arguments;
        if (!timeout) {
            timeout = setTimeout(function(){
                timeout = null;
                func.apply(context, args)
            }, wait)
        }

    }
}

为了让效果更加明显,我们设置 wait 的时间为 3s,效果演示如下:

使用定时器

我们可以看到:当鼠标移入的时候,事件不会立刻执行,晃了 3s 后终于执行了一次,此后每 3s 执行一次,当数字显示为 3 的时候,立刻移出鼠标,相当于大约 9.2s 的时候停止触发,但是依然会在第 12s 的时候执行一次事件。

所以比较两个方法:

  1. 第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行
  2. 第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件

双剑合璧

那我们想要一个什么样的呢?

有人就说了:我想要一个有头有尾的!就是鼠标移入能立刻执行,停止触发的时候还能再执行一次!

所以我们综合两者的优势,然后双剑合璧,写一版代码:

// 第三版
function throttle(func, wait) {
    var timeout, context, args, result;
    var previous = 0;

    var later = function() {
        previous = +new Date();
        timeout = null;
        func.apply(context, args)
    };

    var throttled = function() {
        var now = +new Date();
        //下次触发 func 剩余的时间
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
         // 如果没有剩余的时间了或者你改了系统时间
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
        } else if (!timeout) {
            timeout = setTimeout(later, remaining);
        }
    };
    return throttled;
}

效果演示如下:

throttle3

我们可以看到:鼠标移入,事件立刻执行,晃了 3s,事件再一次执行,当数字变成 3 的时候,也就是 6s 后,我们立刻移出鼠标,停止触发事件,9s 的时候,依然会再执行一次事件。

优化

但是我有时也希望无头有尾,或者有头无尾,这个咋办?

那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:

leading:false 表示禁用第一次执行
trailing: false 表示禁用停止触发的回调

我们来改一下代码:

// 第四版
function throttle(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function() {
        previous = options.leading === false ? 0 : new Date().getTime();
        timeout = null;
        func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function() {
        var now = new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
    };
    return throttled;
}

取消

在 debounce 的实现中,我们加了一个 cancel 方法,throttle 我们也加个 cancel 方法:

// 第五版 非完整代码,完整代码请查看最后的演示代码链接
...
throttled.cancel = function() {
    clearTimeout(timeout);
    previous = 0;
    timeout = null;
}
...

注意

我们要注意 underscore 的实现中有这样一个问题:

那就是 leading:falsetrailing: false 不能同时设置。

如果同时设置的话,比如当你将鼠标移出的时候,因为 trailing 设置为 false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立刻执行,就违反了 leading: false,bug 就出来了,所以,这个 throttle 只有三种用法:

container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
    leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
    trailing: false
});

至此我们已经完整实现了一个 underscore 中的 throttle 函数,恭喜,撒花!

演示代码

相关的代码可以在 Github 博客仓库 中找到

专题系列

JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

@liuxinqiong
Copy link

@liuxinqiong liuxinqiong commented Jul 12, 2017

沙发!!!

@BobLiu1990
Copy link

@BobLiu1990 BobLiu1990 commented Aug 23, 2017

第三版
if (remaining <= 0 || remaining > wait) {
这里的remaining好像不会出现大于wait的情况吧
毕竟是基于wait去减的

@lindazhang102
Copy link

@lindazhang102 lindazhang102 commented Aug 23, 2017

var now = +new Date(); 请问这里为什么要有个加号?

@BobLiu1990
Copy link

@BobLiu1990 BobLiu1990 commented Aug 23, 2017

@lindazhang102 这个是转时间戳的方法

@jawil
Copy link

@jawil jawil commented Aug 23, 2017

这是隐式转换的玄学。

我们先看看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)

JS引擎内部转换为原始值ToPrimitive(obj,preferredType)函数接受两个参数,第一个obj为被转换的对象,第二个
preferredType为希望转换成的类型(默认为空,接受的值为Number或String)

在执行ToPrimitive(obj,preferredType)时如果第二个参数为空并且obj为Date的事例时,此时preferredType会
被设置为String,其他情况下preferredType都会被设置为Number如果preferredType为Number,ToPrimitive执
行过程如
下:
1. 如果obj为原始值,直接返回;
2. 否则调用 obj.valueOf(),如果执行结果是原始值,返回之;
3. 否则调用 obj.toString(),如果执行结果是原始值,返回之;
4. 否则抛异常。

如果preferredType为String,将上面的第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.分析完毕,从原理得出结果,而不是从浏览器输出的结果来解释结果。用结论解释结论,会忽略很多细节,装个逼,逃,233333

@lindazhang102 @WittyBob

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Aug 24, 2017

@WittyBob 如果你修改了系统时间,就会产生 remaining > wait 的情况……

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Aug 24, 2017

@lindazhang102 正如 @jawil 所说,是利用隐式类型转换将时间对象转换为时间戳,类似的还有:

+'1' // 1 (转数字)
1 + '' // '1' (转字符串)
!!1 // true (转布尔值)
@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Oct 25, 2017

@Awzp 置为空是为了 js 的垃圾回收,不过 later 函数中的 timeout 判断其实没有必要,估计是 underscore 在多次修改后忽略了这个问题~

@ClausClaus
Copy link

@ClausClaus ClausClaus commented Nov 6, 2017

厉害了我的哥。。。

@dbfterrific
Copy link

@dbfterrific dbfterrific commented Nov 15, 2017

you are really something my brother!

@ishowman
Copy link

@ishowman ishowman commented Nov 15, 2017

timeout = null;是为了清楚闭包产生的变量常驻内存问题是吧?除了将变量重新赋值为null可以清楚使内存回收机制清楚变量占用的变量,赋值为undefined能清楚内存占用吗?

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Nov 15, 2017

@ishowman 可以,但是我个人认为 undefined 是一个值,而 null 表示无,所以赋值为 undefined 其实是将值改成一个非常小的占用内存的值,效果上跟赋值为 null 还是差了一点……

@zhangruinian
Copy link

@zhangruinian zhangruinian commented Nov 16, 2017

那为什么第二种置为空进行垃圾回收,时间戳的第一种就没有置为空呀,真心请教,疑惑

@zhangruinian
Copy link

@zhangruinian zhangruinian commented Nov 16, 2017

所以timeout = null主要是为了使其为空然后下次好接着执行?

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Nov 17, 2017

@zhangruinian 正是如此,使用定时器时置为空的主要目的并不是垃圾回收,主要是为了方便下次执行定时器

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Nov 17, 2017

@ClausClaus @dbfterrific 这应该是一个意思吧~ 😂

default

@xue1234jun
Copy link

@xue1234jun xue1234jun commented Nov 20, 2017

节流和去抖如何区分

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Nov 20, 2017

@xue1234jun 防抖是虽然事件持续触发,但只有等事件停止触发后 n 秒才执行函数,节流是持续触发的时候,每 n 秒执行一次函数

@xue1234jun
Copy link

@xue1234jun xue1234jun commented Nov 21, 2017

@mqyqingfeng 非常感谢

@savoygu
Copy link

@savoygu savoygu commented Nov 29, 2017

我们要注意 underscore 的实现中有这样一个问题:
那就是 leading:false 和 trailing: false 不能同时设置。
如果同时设置的话,比如当你将鼠标移出的时候,因为 trailing 设置为 false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立刻执行,就违反了 leading: false,bug 就出来了

其一: underscore 怎么没有考虑 leading = falsetrailing = false 同时为 false 的情况,这应该不难实现吧,关键就是 previous 置为初始值 0。

其二:

    var throttled = function() {
        var now = new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
    };

remaining > wait 情况的发生是 now - previous 为负值的时候,也就是获取的当前时间小于先前获取的当前时间,在事件触发的过程中更改当前时间吗(这种概率是有多小)

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Dec 1, 2017

@savoygu 关于第一个问题,其实有人提过这个问题,这是当时的一个核心贡献者的回答:

default

这个开发者认为两者必须要有一个为 true。

除了这个 issue 外,如果要知道 underscore 是否考虑了 leading = false 和 trailing = false 同时为 false 的情况,其实看测试用例就可以了,这是地址 https://github.com/jashkenas/underscore/blob/08361d41590ff35be44ec6b757361ac37f6fa7c7/test/functions.js

搜索 leading ,其实没有两个都为 false 的测试用例。

关于是否容易实现这个效果,如果我们在已有的代码上进行修改,比如 previous 设置为 0,那什么时候设置这个 previous 呢?欢迎补充 demo 哈~

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Dec 1, 2017

@savoygu 关于第二个问题,让我们具体看看这个 PR

default

第一个人提出了这个需求,第二个人认为这个需求太小众,但是 underscore 作者接了这个需求~😂

@savoygu
Copy link

@savoygu savoygu commented Dec 1, 2017

@mqyqingfeng 感谢

@zjp6049
Copy link

@zjp6049 zjp6049 commented Dec 6, 2017

要是有代码的思路就更好了~~~~~~~~~~~~~~~~~

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Dec 6, 2017

@zjp6049 嗯嗯,这个日后补充~

@jxZhangLi
Copy link

@jxZhangLi jxZhangLi commented Dec 8, 2017

// 第二版
function throttle(func, wait) {
    var timeout;
    var previous = 0;

    return function() {
        context = this;
        args = arguments;
        if (!timeout) {
            timeout = setTimeout(function(){
                timeout = null;
                // 删除  func.apply(context, args)
            }, wait)
           // 添加 
            func.apply(context, args)
        }

    }
}

这样就可以立刻执行了。

@dowinweb
Copy link

@dowinweb dowinweb commented Dec 8, 2017

感觉later函数里的这里判断没啥意义啊,求指教
previous = options.leading === false ? 0 : new Date().getTime();
直接previous = +new Date()不行吗

@xiaolee55
Copy link

@xiaolee55 xiaolee55 commented Feb 12, 2020

唯一的疑问是,这个判断什么时候会进去?

if (timeout) {
  clearTimeout(timeout);
  timeout = null;
 }

因为setTimeOut有误差,所以有可能到时间了定时器还没有执行,就会进入时间戳判断逻辑,所以要把定时器删掉

@sunnyBob
Copy link

@sunnyBob sunnyBob commented Feb 25, 2020

    var later = function() {
        previous = options.leading === false ? 0 : new Date().getTime();
        timeout = null;
        func.apply(context, args);
        if (!timeout) context = args = null;
    };

前边给 timeout 赋值为 null 了,接着 if 判断 if (!timeout) context = args = null;,这个判断一定会执行吧?

@pumpkinduan
Copy link

@pumpkinduan pumpkinduan commented Mar 6, 2020

 var later = function() {
        previous = +new Date();
        timeout = null;
        func.apply(context, args)
  };

请问这里为什么要将previous = +new Date();呢?

@xujinfengM
Copy link

@xujinfengM xujinfengM commented Mar 7, 2020

唯一的疑问是,这个判断什么时候会进去?

if (timeout) {
  clearTimeout(timeout);
  timeout = null;
 }

@DFLovingWM
因为定时器并不是准确的时间,很可能你设置了2秒
但是他需要2.2秒才触发,这时候就会进入这个条件

@coderzym
Copy link

@coderzym coderzym commented Mar 13, 2020

有一个地方不是很懂,为什么是args = arguments,这样的话,args不也是类数组吗?为什么不用...args,有些困惑,希望大佬们能够解答

@bosens-China
Copy link

@bosens-China bosens-China commented Apr 7, 2020

    var later = function() {
        previous = options.leading === false ? 0 : new Date().getTime();
        timeout = null;
        func.apply(context, args);
        if (!timeout) context = args = null;
    };

if (!timeout) context = args = null;这一句没必要加上if吧,上面已经设置为null

@bosens-China
Copy link

@bosens-China bosens-China commented Apr 7, 2020

有一个地方不是很懂,为什么是args = arguments,这样的话,args不也是类数组吗?为什么不用...args,有些困惑,希望大佬们能够解答

因为是es6的语法,需要经过转义,你可以看作者写的代码全部用的var声明,默认是es5的环境,他这样做就是想让代码全部兼容,除了这个你想怎么写都OK

@bosens-China
Copy link

@bosens-China bosens-China commented Apr 7, 2020

 var later = function() {
        previous = +new Date();
        timeout = null;
        func.apply(context, args)
  };

请问这里为什么要将previous = +new Date();呢?

给他转化为时间戳,你可以自己实现以下,这个是隐士转换

@lhz960904
Copy link

@lhz960904 lhz960904 commented Apr 29, 2020

自己做的 可视化的测试JS的防抖和节流的效果 链接

demo是参考了国外文章里面的示例,并增加了可修改参数的一些交互。

除了冴羽大佬这俩篇文章,也可以看看上面的这篇国外文章,也不错。

@KeithChou
Copy link

@KeithChou KeithChou commented Jun 9, 2020

这里给一下另外一个版本的throttle,感觉会清晰简单些。能够实现如下内容

  1. 函数节流处理
  2. 立即执行回调函数
  3. 函数脱离事件后,还会执行一次

以下是实现代码哈。

function throttle (fn: Function, delay: number, immediate: boolean): Function {
    var timer: number = null
    var start: number = +new Date()
    return function (): void {
        window.clearTimeout(timer)
        var context: Function = this
        var now: number = +new Date()
        var arr: void[] = []
        var result: void = null
        for (var i: number = 0; i < arguments.length; i++) arr.push(arguments[i])
        // 是否需要立即执行
        if (immediate) {
            // 只执行第一次
            immediate = false
            fn.apply(context, arr)
        } else {
            if (now - start >= delay) {
                start = now
                result = fn.apply(context, arr)
            } else {
                // 函数脱离事件后仍然会执行
                timer = window.setTimeout(function () {
                    fn.apply(context, arr)
                }, delay)
            }
        }
        return result
    }
}
@xsfxtsxxr
Copy link

@xsfxtsxxr xsfxtsxxr commented Jun 18, 2020

唯一的疑问是,这个判断什么时候会进去?

if (timeout) {
  clearTimeout(timeout);
  timeout = null;
 }

@DFLovingWM
因为定时器并不是准确的时间,很可能你设置了2秒
但是他需要2.2秒才触发,这时候就会进入这个条件

为何你们都如此优秀,考虑的风险条件这么周全,羡慕!

@praywj
Copy link

@praywj praywj commented Aug 21, 2020

第三版
if (remaining <= 0 || remaining > wait) {
这里的remaining好像不会出现大于wait的情况吧
毕竟是基于wait去减的

@BobLiu1990
修改了系统时间可能会导致now<previous

@tancgo
Copy link

@tancgo tancgo commented Oct 29, 2020

第一版时间戳实现的话 并不需要context = this这个步骤吧 this能正确拿到

@woodThunder
Copy link

@woodThunder woodThunder commented Oct 30, 2020

你好,节流内部有异步请求时,节流失效了,是什么问题呢?previous赋值失效,一直是0

@xin0320
Copy link

@xin0320 xin0320 commented Nov 24, 2020

// 第二版
function throttle(func, wait) {
    var timeout;
    var previous = 0;

    return function() {
        context = this;
        args = arguments;
        if (!timeout) {
            timeout = setTimeout(function(){
                timeout = null;
                // 删除  func.apply(context, args)
            }, wait)
           // 添加 
            func.apply(context, args)
        }

    }
}

这样就可以立刻执行了。

但是这样事件停止了就不会再触发最后一次了

@Jiang1996
Copy link

@Jiang1996 Jiang1996 commented Dec 3, 2020

 var later = function() {
        previous = +new Date();
        timeout = null;
        func.apply(context, args)
  };

请问这里为什么要将previous = +new Date();呢?
转成时间戳

@HaofangLiu
Copy link

@HaofangLiu HaofangLiu commented Mar 19, 2021

这是隐式转换的玄学。

我们先看看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)

JS引擎内部转换为原始值ToPrimitive(obj,preferredType)函数接受两个参数,第一个obj为被转换的对象,第二个
preferredType为希望转换成的类型(默认为空,接受的值为Number或String)

在执行ToPrimitive(obj,preferredType)时如果第二个参数为空并且obj为Date的事例时,此时preferredType会
被设置为String,其他情况下preferredType都会被设置为Number如果preferredType为Number,ToPrimitive执
行过程如
下:
1. 如果obj为原始值,直接返回;
2. 否则调用 obj.valueOf(),如果执行结果是原始值,返回之;
3. 否则调用 obj.toString(),如果执行结果是原始值,返回之;
4. 否则抛异常。

如果preferredType为String,将上面的第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.分析完毕,从原理得出结果,而不是从浏览器输出的结果来解释结果。用结论解释结论,会忽略很多细节,装个逼,逃,233333

@lindazhang102 @WittyBob

请教:
在控制台尝试typeof new Date().toString() 是string,根据前面的结果这个时候不应该返回‘Sun Feb 28 2016 13:40:36 GMT+0800 (中国标准时间)’ ,为什么这之后还会去执行valueOf()然后返回时间戳呢?

image

感激回答!

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

Successfully merging a pull request may close this issue.

None yet