10.JS进阶

198 阅读21分钟

原型和原型链

  • 所有对象都是通过new 函数创建
  • 所有的函数也是对象
    • 函数中可以有属性,方法也可以叫做属性。
  • 所有对象都是引用类型

原型 prototype

所有函数都有一个属性:prototype,称之为函数原型。函数原型自然主语自然是函数,只要函数对象才有的属性,非函数对象没有这个属性。

默认情况下,prototype是一个普通的Object对象

默认情况下,prototype中有一个属性,constructor,它也是一个对象,它指向构造函数本身。

 function Person(name, age){ 
    this.name = name;
    this.age = age;
 }
 
 Person.prototype.motherland = 'China'
 

//这是一条很重要的准则 Person.prototype.constructor == Person

准则1:原型对象(即Person.prototype)的constructor指向构造函数本身

一个创建对象的问题

// 创建的是test对象
function test(){

}
var obj = new test();
// 创建的是Object对象,和下面的代码同样的效果
function test(){
           return {}; 
        }
 

function test(){
   return new Object(); 
}
        
var obj = new test();

隐式原型 proto

所有的对象都有一个属性:__proto__,称之为隐式原型

默认情况下,隐式原型指向(创建该对象的函数)的原型。也就是可以理解为他是通过哪个函数创建的,当前对象的__proto__就指向该函数的原型。

当访问一个对象的成员时:

  1. 看该对象自身是否拥有该成员,如果有直接使用
  2. 在原型链中依次查找是否拥有该成员,如果有直接使用

猴子补丁:在函数原型中加入成员,以增强起对象的功能,猴子补丁会导致原型污染,使用需谨慎。

原型链

特殊点:

  1. Function的__proto__指向自身的prototype
  2. Object的prototype的__proto__指向null

下面几条

1.对象都是由函数创建,函数(也是对象)都是由Function函数来创建的。

函数是通过new Function创建的.jpg

普通对象是通过new 函数创建的.jpg

2.每一个函数都有原型对象,即每一个函数对象都有一个property属性,property属性也是一个对象。而property这个属性对象里面都有一个constructor,而constuctor指向当前函数本身,也就是函数名称。

每个函数都有原型对象.jpg

原型中的constructor指向函数本身.jpg

3.所有普通对象和函数对象都有__proto__这个隐式原型,(注意和函数对象有property这个原型做对比),也就是普通对象和函数对象都有 __proto__隐式原型,而只有函数对象有property这个原型。 而隐式原型代表的意思是创建该对象的函数原型。__proto__获取的都是函数的原型。

  1. 注意的是自定义函数是有Function函数创建的
  2. 注意Function的隐式原型指向Object,但是Object 却是 Function new 出来的

链条的全貌.jpg

现在我们知道了,当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会通过它的__proto__隐式属性,找到它的构造函数原型对象,如果还没有找到就会再在其构造函数prototype__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链

其实就是先找自身的,自身找不到,就找他的隐式原型里面找,隐式原型找不到就去找隐式原型的隐式原型去找。隐式原型其实就是创建他这个对象的构造函数的原型。

覆盖的问题

array 在构造函数的原型上重写tostring的方法,那么在Object对象上的原型就没有机会被调用了,这就是覆盖的问题,类似oc里面的重写了某个方法。 如果在array使用object的方法,应该怎么做呢?

1.先找到原来tostring方法的实现

2.使用call改变this指向

具体如下图参考的那样 image.png

原型,隐式原型的面试题

1.第一题

var F = function () {}
Object.prototype.a = function () {}
Function.prototype.b = function () {}

var f = new F();

console.log(f.a, f.b, F.a, F.b);
// fn    undefined    fn    fn

解读:可以看到上面的代码第二行给Object的原型添加了属性a,Function的原型添加了属性b。
f是自定义对象,他的原型链是自定义对象--->自定义函数的原型---->Object原型---> null
F是自定义函数,他的原型链是 自定义函数--->Function原型--->Object原型--->null
由于不论自定义函数还是自定义对象,他们的原型链上都有Object原型,所以,他们两个都有a这个属性。
而b属性是在Function原型上,所以只有自定义函数上面有,所有只有F.b有。

特别注意区别的是F.prototype 和 Function.prototype的区别,一个是自定义函数的原型,一个是Function的原型。

2.第二题

function A() {}
function B(a) {
    this.a = a;
}
function C(a) {
    if (a) {
        this.a = a;
    }
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;

console.log(new A().a); //1
console.log(new B().a); //undefined
console.log(new C(2).a); //2

解读:

先查找自身的属性,自身的属性没有,会去隐式原型去找,隐式原型继续顺着原型链去找。
new A().a 原型上可以找到是1.
new B().a 自身属性可以找到是undefined.
new C(2).a 自身属性可以找到是2,没还没有轮到去原型链找。

3.第三题

function User() {}
User.prototype.sayHello = function() {}

var u1 = new User();
var u2 = new User();

console.log(u1.sayHello === u2.sayHello); //true
console.log(User.prototype.constructor); //User Function
console.log(User.prototype === Function.prototype); // false
console.log(User.__proto__ === Function.prototype); // true
console.log(User.__proto__ === Function.__proto__); // true
console.log(u1.__proto__ === u2.__proto__);  // true
console.log(u1.__proto__ === User.__proto__); // false
console.log(Function.__proto__ === Object.__proto__); // true
console.log(Function.prototype.__proto__ === Object.prototype.__proto__); // false
console.log(Function.prototype.__proto__ === Object.prototype); // true

解读:

  1. u1.sayHello === u2.sayHello 均是对象的隐式原型上找到的,是同一个
  2. User.prototype.constructor 构造函数的原型里面的constructor 指向构造函数本身
  3. User.prototype === Function.prototype 左边是自定义构造函数的原型,右边是Function 的原型。
  4. User.__proto__ === Function.prototype 左边是自定义构造函数的隐式原型,可以理解为谁创建了自定义构造函数,是Function创建了自定义构造函数, User.__proto__就代表自定义构造函数的隐式原型是Function函数的原型。
  5. User.__proto__ === Function.__proto__ ,这里需要注意一点的是Function的原型和隐式原型都指向Function的原型。所以这里和4是一样的结果。
  6. u1.__proto__ === u2.__proto__ 左右两边其实都是User的原型
  7. Function.__proto__ === Object.__proto__ 左边其实就是Function的原型,Object的构造函数是由Function创建的,因此Object.__proto__ 的隐式原型其实就是Function的原型.
  8. Function.prototype.__proto__ === Object.prototype.__proto__ 左边其实代表的是Function的原型的隐式原型,其实就是Object的原型, Object.prototype.__proto__其实是null。
  9. 看8的解释。

静态方法,原型方法,实例方法

Snip20240108_1.png

// 构造函数
function People(color) {
    this.color = color;
    this.seeColor = function() {
        console.log("color is "+this.color)
    }
}
// 静态方法
People.say = function() {
    console.log("静态方法color is ", this.color)
}
// 原型方法
People.prototype.eat = function() {
    console.log("原型方法color is ", this.color)
}
// 实例
var pp = new People();

// 调用静态方法
People.say(); // 静态方法color is  undefined
pp.say(); // 报错信息:Uncaught TypeError: pp.say is not a function

// 调用原型方法
People.eat(); // 报错信息:Uncaught TypeError: People.eat is not a function
pp.eat(); // 原型方法color is  Red

// 调用实例方法
People.seeColor(); // Uncaught TypeError: People.seeColor is not a function
pp.seeColor(); // color is Red

总结:

1.实例方法,其实就是普通对象的方法,他的方法只能被普通对象所访问。

2.静态方法,可以理解为People这个类的方法,可以类比为类方法,这个只能被People这个函数对象调用,不能被实例对象调用。同样可以参照类方法不能被对象调用来理解。

3.原型方法,原型方法可以被实例直接访问或者People这个函数对象通过property来访问。

原型链的应用

基础方法

W3C不推荐直接使用系统成员__proto__,可以使用原型,但是尽量不要直接使用隐式原型。

Object.getPrototypeOf(对象)

获取对象的隐式原型,相当于 object.proto 一样的功能。上面是一个静态方法,静态方法意味着是不能用 obj.getPrototypeOf 调用,而是要用Object去调用。

Object.prototype.isPrototypeOf(对象)

判断当前对象(this)是否在指定对象的原型链上

A.prototype.isPrototypeOf(B)表达的意思是A对象的原型是不是在B的原型链上,A.property 代表的是A的原型, isPrototypeOf代表的是不是在B的原型链上。

对象 instanceof 函数

判断函数的原型是否在对象的原型链上 通俗的举个例子就是

b instanceof Array

上面判断的b是不是一个Array的实例,上面那句话 判断函数的原型是否在对象的原型链上 可以说成对象的原型链上是不是有函数对象的原型,如果有的话,就是Array这种类型。

Object.create(对象)

创建一个新对象,其隐式原型指向指定的对象 上面其实就是两个步骤:

  1. 创建一个对象
  2. 设置隐式原型指向这个对象(有点类似于设置isa指针)

所有的对象的原型链上都有Object的原型,或者说所有的对象都是Object类型,或者说请创建一个对象,不是Object类型的。都可以参照下面的代码

a的原型直接指向null,所以就是上面说的特殊情况。

Object.prototype.hasOwnProperty(属性名)

判断一个对象自身是否拥有某个属性,不是原型链上面有的

应用

类数组转换为真数组

Array.prototype.slice.call(类数组);

不用 [].slice.call(类数组);是因为他凭空要创建一个空的数组,而创建空的数组的目的是为了调用slice的方法,完全可以去array的原型去找到。

实现继承

默认情况下,所有构造函数的父类都是Object 自定义构造函数继承于Object,Object是自定义构造函数的父类,自定义构造函数是自定义构造函数的子类。

圣杯模式

image.png

上面1,2,3是完成继承的最核心工作,只要三个箭头完成就实现了继承。

son.prototype = Object.create(father.prototype);

  1. Object.create(father.prototype),创建一个新对象,其隐式原型指向father.prototype.这样第一步就完成了。
  2. 将上面的值赋值给son.prototype,第二步完成。
  3. son.prototype.constructor = son; 这句代码完成第三步。

标准圣杯模式写法

image.png

image.png

一种更好的圣杯模式写法

image.png

image.png

之所以说上面的代码更好,是因为son.prototype.uber 指向father这个构造函数,而不是father的原型,因为这样写,就让son对象拥有了father的创建函数,在创建的时候就可以直接使用this.uber(firstName,lastName,age);创建了。而不再需要绑定this了,是因为this.uber就直接拿到了father.其实这里的this.uber就相当于super关键字。

最通用最牛逼的一种圣杯模式写法

this.myPlugin.inherit = (function () {
    var Temp = function () { }
    return function (son, father) {
        Temp.prototype = father.prototype;
        son.prototype = new Temp();
        son.prototype.constructor = son;
        son.prototype.uber = father.prototype;
    }
}());

解读: Temp 本来是一个普通的函数,Temp.prototype = father.prototype;一句话产生巨大的效果.

image.png

image.png

属性描述符

属性描述符的配置参考:developer.mozilla.org/zh-CN/docs/…

属性描述符:它表达了一个属性的相关信息(元数据),它本质上是一个对象。

  1. 数据属性
  2. 存取器属性
    1. 当给它赋值,会自动运行一个函数
    2. 当获取它的值时,会自动运行一个函数

最简单使用

 function User(name, age) {
            this.name = name;
            //年龄的取值范围是 0 - 100
            //如果年龄的值小于了0,则赋值为0,如果年龄的值大于了100,则赋值为100
            var _age;
            Object.defineProperty(this, "age", {
                get: function() {
                    return _age;
                },
                set: function(val) {
                    if (val < 0) {
                        val = 0;
                    } else if (val > 100) {
                        val = 100;
                    }
                    _age = val;
                }
            })

            this.age = age;
        }

        var u = new User("abc", -1);
        u.age = u.age + 10000;
        console.log(u.age);

其他使用方法

var config = {
            _x: 0,
            _y: 0,
            xDis: 2,
            yDis: 2,
            duration: 16,
            width: 100,
            height: 100
        }

        Object.defineProperty(config, "x", {
            get: function() {
                return this._x;
            },
            set: function(val) {
                if (val < 0) {
                    val = 0;
                } else if (val > document.documentElement.clientWidth - this.width) {
                    val = document.documentElement.clientWidth - this.width;
                }
                this._x = val;
                div.style.left = val + "px";
            }
        })

Object.defineProperty 的其他使用

var obj = {
            x: 1,
            y: 2
        };
        
Object.defineProperty(obj, "name", {
        value: "abc",
        writable: false,
        enumerable: true //不可迭代 遍历
    })
      

其他的属性描述符

Object.getOwnPropertyDescriptor

获取某个对象的某个属性的属性描述符对象(该属性必须直接属于该对象)

执行上下文

函数执行上下文:一个函数运行之前,创建的一块内存空间,空间中包含有该函数执行所需要的数据,为该函数执行提供支持。

执行上下文栈:call stack,所有执行上下文组成的内存空间。

栈:一种数据结构,先进后出,后进先出。

全局执行上下文:所有JS代码执行之前,都必须有该环境。

JS引擎始终执行的是栈顶的上下文。

执行上下文中第一件事是要先确定this的指向

执行上下文中的内容

  1. this指向

1). 直接调用函数,this指向全局对象 2). 在函数外,this指向全局对象 3). 通过对象调用或new一个函数,this指向调用的对象或新对象

  1. VO 变量对象

Variable Object:VO 中记录了该环境中所有声明的参数、变量和函数

Global Object: GO,全局执行上下文中的VO

Active Object:AO,当前正在执行的上下文中的VO

1). 确定所有形参值以及特殊变量arguments 声明和形参名称一样的变量,并且给变量赋值为实参的值

2). 确定函数中通过var声明的变量,将它们的值设置为undefined,如果VO中已有该名称,则直接忽略。 如果参数已经有了这个声明的变量,这个已经有的变量可能是在形参声明的,也可能是函数中(声明var同名变量的位置比当前的位置靠前)通过var声明的,这里说的忽略是指的是忽略再声明的过程和值设为undefined的过程

3). 确定函数中通过字面量声明的函数,将它们的值设置为指向函数对象,如果VO中已存在该名称,则覆盖。

a.这里的函数必须是字面量声明的函数。什么是字面量声明的函数呢? 比方说 function A() { }var A = function () { } 就不是字面量声明的函数。

b.如果前面的形参和var变量还没声明到和函数名称相同的变量,则这里会直接声明一个函数的变量。比方说 A:function A() { } 如果遇到前面已经有了和函数名称一样的变量,这里包括前面的参数和var变量声明的,都会直接把这个变量的值改为 指向函数的地址。为什么函数高级一些呢,因为函数是一等公民。

当一个上下文中的代码执行的时候,如果上下文中不存在某个属性,则会从之前的上下文寻找。

下面分析几个题目

第一题

  <script>
        var g1 = 123;
        function A(a, b) {
            console.log(a, b, g1);
            var b = 123;
            

            function b() {}

            var a = function() {}

            
            
        }
        var g2 = 456;

        var g3 = function() {}

        A(1, 2);
    </script>

image.png

下面详细分析一下: 首先分析全局的GO的情况,里面有g1,g2,g3,还有一个函数的字面量声明,再添加一个fn变量。

继续往下看,到了A(1,2) 这一句,于是产生了新的调用过程,创建一个新的VO,也就是上图的AO,继续分析AO中有什么?先是第一步创建形参的两个变量 a:1 ,b:2.

继续看函数A中var声明的变量,函数中有 var b var a这两个变量声明,本来的步骤应该是继续声明 a = undefined b= undefined ,根据上面的第二条规则,变量名和形参名重名,则忽略这个过程。

下面开始第三个过程,看函数的字面量声明,这里可以看到有function b {},虽然和前面声明的变量有冲突,但是由于函数是一等公民,这里将b:fn(函数b的地址)

这就是是上图结果的原因。

下面开始看执行阶段,先是看 console.log(a, b, g1); 此时 a:1 b:fn, g1 从自己的找不到就往上找,可以从前一个VO中找到g1 = 123,

接着往下看 var b = 123;这一句忽略定义,只看赋值,此时将 b:fn 变成了 b:123,

继续往下看 function b() {} 这一句完全是定义,不管,继续往下看。

继续往下看 var a = function() {},忽略定义,只看赋值,这里将 a赋值为fn

最后一句赋值,就是 console.log(a, b); a为fn,b为123.

第二题

var foo = 1;
function bar() {
    console.log(foo);  //undefined
    if (!foo) {
        var foo = 10;
    }
    console.log(foo); //10
}

bar();

***只要不是函数里面套函数的中声明var 变量,在考虑当前VO的时候也要考虑,也需要按照上面的方案二的规则去执行 ***

分析: 全局上下文GO: foo : undefined---> foo----> 1 bar :fn

bar的VO: foo :undefined 执行了

if (!foo) {
        var foo = 10;
    }

后foo:10

第三题

在考虑第三条的时候,去确定vo的时候不要去考虑函数的实际执行,具体这个题目来说就是虽然return 永远不会执行,但是考虑a函数的时候仍然要按照第三条规则去执行。

var a = 1;
function b() {
    console.log(a);  // fn
    a = 10;
    return;
    function a() { }
}
b();
console.log(a); //1

第四题

严格按照确定 vo 和代码执行两个阶段去考虑就没有问题

console.log(foo); //fn  C
var foo = "A";
console.log(foo)  //A
var foo = function () {
    console.log("B");
}
console.log(foo); //fn  B
foo(); // B
function foo(){
    console.log("C");
}
console.log(foo)  //fn B
foo(); // B

第五题

在当前VO确定完后,所有形参和变量的值都是从当前VO中去取的而不涉及到原来的形参等东西

var foo = 1;
function bar(a) {
    var a1 = a;
    var a = foo;
    function a() {
        console.log(a); //1
    }
    a1();
}

bar(3);
 

GO: foo:1 bar:fn

AO:
a = 3;----> a:fn ---------------> a = 1 a1 = undefined; ---> a1 = fn

下面开始执行代码 执行 var a1 = a; 这句的意思是 将 a1 等于当前 a的值,而不是说 a1 = 等于形参 a1 的值, 继续下面的代码 a = 1; 最后执行输出a的值为1.

作用域链

  1. VO中包含一个额外的属性,该属性指向创建该VO的函数本身
  2. 每个函数在创建时,会有一个隐藏属性[[scope]],它指向创建该函数时的AO
  3. 当访问一个变量时,会先查找自身VO中是否存在,如果不存在,则依次查找[[scope]]属性。

某些浏览器会优化作用域链,函数的[[scope]]中仅保留需要用到的数据。

题目1

var g = 0;

function A() {
    var a = 1;

    function B() {
        var b = 2;
        var C = function() {
            var c = 3;
            console.log(c, b, a, g);
        }
        C();
    }

    B();
}

A();

image.png

题目二 闭包的经典实现原理

    var count = 100;
    function A() {
        var count = 0;
        return function() {
            count++;
            console.log(count);
        }
    }

    var test = A();

    test();
    test();
    test();

    console.log(count);

image.png

下图是闭包的经典实现原理图

image.png

其实就是在test=A();这句代码执行完后function A的VO就应该销毁了,但是由于全局环境的test能找到返回的匿名函数,而匿名函数的scope又指向这个vo,这样count本来在A声明中的变量就保存下来了,这样就可以使用test()来使用count,这就是闭包的经典实现原理。

题目三


    var a = 1;

    function A() {
        console.log(a);
    }

    function Special() {
        var a = 5;
        var B = A;
        B();
    }

    Special();
  

image.png

题目四

var foo = { n: 1 };
(function (foo) {
    console.log(foo.n); // 1
    foo.n = 3;
    var foo = { n: 2 };
    console.log(foo.n); // 2
})(foo);
console.log(foo.n); // 3

image.png

题目五

var food = "rice";
var eat = function () {
    console.log(`eat ${food}`);
};
(function () {
    var food = "noodle";
    eat();//eat rice
})();

image.png

题目六

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

A();

console.log(i);

image.png

image.png

题目七

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

image.png

总结:

1.函数可以使用外边的东西

2.外边的东西是以当时声明的时间的变量值为准,而不是以运行和调用的时候为准。

事件循环

异步:某些函数不会立即执行,需要等到某个时机成熟后才会执行,该函数叫做异步函数。

浏览器的线程:

  1. JS执行引擎:负责执行JS代码,只有它能执行js代码
  2. 渲染线程:负责渲染页面
  3. 计时器线程:负责计时
  4. 事件监听线程:负责监听事件
  5. http网络线程:负责网络通信

异步函数包括:用户的操作(就是事件监听),计时,ajax(网络通信);

事件队列:一块内存空间,用于存放执行时机到达的异步函数。当JS引擎空闲(执行栈没有可执行的上下文),它会从事件队列中拿出第一个函数执行。

只有JS引擎能够执行JS 代码。

只有js引擎空闲才回去看执行队列。

js引擎里面有正在执行的代码,一定要把当前代码的执行完才会去事件队列里面看一下,哪些代码等着被执行。

image.png

事件循环:event loop,是指函数在执行栈、宿主线程、事件队列中的循环移动。

执行栈空的时候拿事件队列去执行,执行栈执行过程可能会像宿主环境发起一个异步的函数,当异步函数条件满足又要把他放到事件队列里面。执行事件队列的函数的时候又到执行栈。

对象混合

/**
 * obj2混合到obj1产生新的对象
 */
this.myPlugin.mixin = function (obj1, obj2) {
    return Object.assign({}, obj1, obj2);
    // var newObj = {};
    // //复制obj2的属性
    // for (var prop in obj2) {
    //     newObj[prop] = obj2[prop];
    // }
    // //找到obj1中有但是obj2中没有的属性
    // for (var prop in obj1) {
    //     if (!(prop in obj2)) {
    //         newObj[prop] = obj1[prop];
    //     }
    // }
    // return newObj;
}

对象混合的一个常用场景是函数的参数,如果函数的参数如果没有传的话,我想给他一个默认值,这种情况我就可以使用对象混合了。

对象克隆

// 分别分为对象,数组去处理,处理到原始类型为止。
/**
 * 克隆一个对象
 * @param {boolean} deep 是否深度克隆
 */
this.myPlugin.clone = function (obj, deep) {
    if (Array.isArray(obj)) {
        if (deep) {
            //深度克隆
            var newArr = [];
            for (var i = 0; i < obj.length; i++) {
                newArr.push(this.clone(obj[i], deep));
            }
            return newArr;
        }
        else {
            return obj.slice(); //复制数组
        }
    }
    else if (typeof obj === "object") {
        var newObj = {};
        for (var prop in obj) {
            if (deep) {
                //深度克隆
                newObj[prop] = this.clone(obj[prop], deep);
            }
            else {
                newObj[prop] = obj[prop];
            }
        }
        return newObj;
    }
    else {
        //函数、原始类型
        return obj; //递归的终止条件
    }
}

防抖

函数防抖可以想象到电梯的场景,只有有人进去,电梯门就要等一段时间才能关闭,进一个人要等这个时间,进去一个人,时间都要重新开始计算。

this.myPlugin.debounce = function (callback, time) {
    var timer;
    return function () {
        clearTimeout(timer);//清除之前的计时
        var args = arguments; //利用闭包保存参数数组
        timer = setTimeout(function () {
            callback.apply(null, args);
        }, time);
    }
}

在函数内部返回一个函数叫做高阶函数。

首先上面方法执行后返回了一个新的函数。可以理解为this.myPlugin.debounce 其实就是下面的代码,这时间如果你执行 his.myPlugin.debounce(width)其实就相当于下面的匿名函数执行 function (width),由于setTimeout里面也是函数,所以这里为了拿到真正传进去的参数,需要执行用args这个保存起来,再去执行。

function () {
        clearTimeout(timer);//清除之前的计时
        var args = arguments; //利用闭包保存参数数组
        timer = setTimeout(function () {
            callback.apply(null, args);
        }, time);
    }
}

有时也会看到下面的写法,其实下面的用了一个ES6的一个语法,把函数的参数收集到args的数组中去。

export default function debounce(fn, duration = 100) {
    let timer = null;
    return function(...args) {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, args);
        }, duration);
    };
}

节流

this.myPlugin.throttle = function (callback, time, immediately) {
    if (immediately === undefined) {
        immediately = true;
    }
    if (immediately) {
        var t;
        return function () {
            if (immediately) {
                if (!t || Date.now() - t >= time) { //之前没有计时 或 距离上次执行的时间已超过规定的值
                    callback.apply(null, arguments);
                    t = Date.now(); //得到的当前时间戳
                }
            }
        }
    }
    else {
        var timer;
        return function () {
            if (timer) {
                return;
            }
            var args = arguments; //利用闭包保存参数数组
            timer = setTimeout(function () {
                callback.apply(null, args);
                timer = null;
            }, time);
        }
    }
}

防抖和节流的区别

1、防抖只执行最后一次操作。

2、节流是在某一段时间内只执行一次操作,这个操作往往是第一次。

防抖和节流辅助记忆

防抖可以想象电梯的案例,只有没有人进来之后,间隔两秒才能电梯门才能关上,而一直有人进来,电梯门永远关不上。

节流可以想象一个火箭发射按钮,而炮弹只能隔两秒才能发射一发,所以你第一次按了火箭发射按钮后,再不停的去按发射按钮,都不会发射。只有过了这2s的时间后按发射按钮才会再发射。

防抖和节流 典型使用场景

防抖

典型场景:搜索框搜索输入

节流

典型场景:高频事件 快速点击、鼠标滑动、resize 事件、scroll。