JS高级 && ES6

77 阅读37分钟

构造函数、原型、原型链

JS中万物皆对象,对象又分为普通对象和函数对象。

  • 普通对象:用函数对象new出来的就是普通对象。也成为实例对象
    let a = {};
    //语法糖
    let a = new Object();
    
    
    let a = []; 
    //语法糖
    let a = new Array();
    
  • 函数对象:JS自带的ObjectFunction,还有new Function()的实例对象(方法)。
    function Foo(){ ... } 
    // 语法糖
    var Foo = new Function(...)
    

构造函数

用来初始化新创建的对象的函数是构造函数。

在使用对象字面量创建一系列同一类型的对象时,这些对象可能具有一些相似的特征(属性)和行为(方法),此时会产生很多重复的代码,而使用构造函数就可以实现代码的复用

  • 命名需要大写。
  • 只能用 new 操作符来执行。
  • 使用 new 关键字调用函数的行为被称为实例化。
  • 无需return,默认返回新创建的对象。
  • 比较浪费内存
function Dog(name, age) {
    this.name = name;
    this.age = age;
}
//实例化
const sure = new Dog('sure', 4);
const cloud = new Dog('cloud', 6);
  • 实例化执行过程

    1. 使用 Dog() 会执行 Dog() 函数。
    2. 使用 new 会创建一个空对象 {}
    3. 使用 new Dog() 是创建一个空对象再执行 Dog() 函数。
    4. thisthis 指向空对象。
    5. this.name 是给空对象添加一个name 属性。
    6. this.name = name则把 Dog() 函数传来的实参赋给 this.name
    7. 返回新对象。
  • 实例成员

    • 构造函数创建的对象实例对象,实例对象中的属性和方法称为实例成员(实例属性、实例方法)。
    • 构造函数创建的实例成员彼此独立互不影响,都有自己独立的属性值和执行方法。
  • 静态成员

    • 构造函数的属性和方法被称为静态成员(静态属性、静态方法)。
    • 静态成员只能通过构造函数访问。
    • 静态方法中的 this 指向构造函数。

存在的问题:每通过构造函数创建一个实例就会开辟一个新内存,但素一般对象中的方法都是复用的,每次都要开一个空间存放方法会很浪费内存。

原型对象以及prototype

构造函数有一个prototype属性,指向实例对象的原型对象。通过同一个构造函数实例化的多个对象具有相同的原型对象。经常使用原型对象来实现继承

JS中只有函数对象有prototype属性,不过每个对象都有自己的原型对象,当我们访问对象的属性和方法时,JS会先访问对象本身的属性和方法,没有则访问其原型对象。

  • 构造函数通过原型分配的函数是所有对象所共享的
  • 所以可以把公共的属性写在构造函数上;公共的方法写在原型上。
  • 构造函数和原型里的this都指向实例化的对象

constructor

  • 原型对象上有prototype属性,指向该构造函数对应的原型对象,原型对象有一个constructor属性,指向该原型对象对应的构造函数。因此constructor和prototype是循环使用的
//Foo构造函数
function Foo(){}
console.log(Foo.prototype.constructor === Foo); // true
  • 由于实例对象可以继承原型对象的属性,所以实例对象也拥有constructor属性,同样指向原型对象对应的构造函数。
//Foo构造函数
function Foo(){}
let foo = new Foo();
console.log(foo.constructor === Foo); // true

proto

实例对象有一个proto属性,指向该实例对象的原型对象。现在浏览器标注的是[[prototype]]

function Foo(){}; 
let f1 = new Foo; 
console.log(foo.__proto__ === Foo.prototype); // true

图解关系

2be7f4954ab6b0d2d35f0c44f3675a2.png

第一部分 Foo

  1. 实例对象f1是通过构造函数Foo()new操作创建的。构造函数Foo()的原型对象是Foo.prototype;实例对象f1通过__proto__属性也指向原型对象Foo.prototype
    function Foo(){}
    let f1 = new Foo();
    console.log(f1.__proto__ === Foo.prototype); // true
    
  2. 实例对象f1本身并没有constructor属性,但它可以继承原型对象Foo.prototypeconstructor属性。
    function Foo(){}
    let f1 = new Foo();
    console.log(Foo.prototype.constructor === Foo); // true
    console.log(f1.constructor === Foo); // true
    //本身没有,是继承而来的
    console.log(f1.hasOwnProperty('constructor')); // false
    

第二部分 Object

  1. Foo.prototypef1原型对象,同时它也是实例对象。实际上,任何对象都可以看做是通过Object()构造函数的new操作实例化的对象

    所以,Foo.prototype作为实例对象,它的构造函数是Object(),原型对象是Object.prototype。相应地,构造函数Object()prototype属性指向原型对象Object.prototype;实例对象Foo.prototypeproto属性同样指向原型对象Object.prototype

    function Foo(){}; 
    let f1 = new Foo(); 
    console.log(Object.prototype.constructor === Object);
    console.log(Foo.prototype.__proto__ === Object.prototype); // true
    
  2. 实例对象Foo.prototype本身具有constructor属性,所以它会覆盖继承自原型对象Object.prototypeconstructor属性。

    function Foo(){};
    console.log(Foo.prototype.hasOwnProperty('constructor')); // true
    
  3. Object.prototype作为实例对象的话,它的原型对象是null

    console.log(Object.prototype.__proto__ === null); // true
    

第三部分 Function

函数也是对象,只不过是具有特殊功能的对象而已。任何函数都可以看做是通过Function()构造函数的new操作实例化的结果。

如果把函数Foo当成实例对象的话,其构造函数是Function(),其原型对象是Function.prototype;类似地,函数Object的构造函数也是Function(),其原型对象是Function.prototype

function Foo(){}; 
let f1 = new Foo; 
console.log(Foo.__proto__ === Function.prototype); // true 
console.log(Object.__proto__ === Function.prototype); // true

原型对象Function.prototypeconstructor属性指向构造函数Function();实例对象ObjectFoo本身没有constructor属性,需要继承原型对象Function.prototypeconstructor属性。

function Foo(){}; 
let f1 = new Foo; 
console.log(Function.prototype.constructor === Function); // true 
console.log(Foo.constructor === Function); // true 
console.log(Foo.hasOwnProperty('constructor')); // false 
console.log(Object.constructor === Function); // true 
console.log(Object.hasOwnProperty('constructor')); // false

所有的函数都可以看成是构造函数Function()new操作的实例化对象。那么,Function可以看成是调用其自身new操作的实例化的结果,所以,如果Function作为实例对象,其构造函数是Function,其原型对象是Function.prototype

console.log(Function.__proto__ === Function.prototype); // true 
console.log(Function.prototype.constructor === Function); // true 
console.log(Function.prototype === Function.prototype); // true

如果Function.prototype作为实例对象的话,其原型对象是什么呢?和前面一样,所有的对象都可以看成是Object()构造函数的new操作的实例化结果。所以,Function.prototype的原型对象是Object.prototype,其原型函数是`Obj

console.log(Function.prototype.__proto__ === Object.prototype); // true

参考作者:Rays77 (juejin.cn/post/703174…)

谢谢大佬的讲解。

原型链

每个实例都有一个原型对象,通过__proto__指向上一级原型对象,并继承其方法和属性,同时原型对象也可能有原型对象,这样一层一层最终指向null,这种关系被称为原型链,null没有原型对象,并作为原型链的最后一个环节。

可以通过instanceof去判断一个对象是否是其原型对象。

function Foo() {};
let foo = new Foo();
console.log(foo instanceof Foo); // true
console.log(foo instanceof Object); // true

垃圾回收机制

垃圾回收机制(Garbage Collection)简称GC,是一种自动内存管理机制,用于检测和清除不再使用的对象,以释放内存空间。js中内存分配回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收,并供其他对象使用。垃圾回收的目的是减少内存泄漏和提高程序的性能

内存的生命周期

js环境中分配的内存,一般有如下生命周期

  1. 内存分配: 当我们声明函数、变量、对象的时候,系统会自动分配内存

  2. 内存使用: 即读写内存,也就是使用变量、函数等

  3. 内存回收: 使用完毕,由垃圾回收器自动回收不再使用的内存

    • 一般情况,全局变量不会被收回(关闭页面回收)
    • 一般情况,局部变量的值使用完就会被自动回收

垃圾产生的原因

  1. 对象不再被引用:当一个对象不再被任何变量或属性引用时,它就成为垃圾。例如,当一个函数执行完毕后,其中创建的局部变量将成为垃圾。
  2. 对象之间形成循环引用:当两个或多个对象相互引用,并且它们之间没有外部引用时,它们将成为垃圾。这种情况下,即使这些对象不再被任何其他代码引用,它们也无法被垃圾回收器清除。
  3. 动态创建的对象没有被及时销毁:如果在代码中频繁地创建新的对象,但没有及时销毁这些对象,就会产生垃圾。特别是在循环或递归等情况下,如果没有正确地释放内存,垃圾会不断积累。
  4. 内存泄漏:程序分配的内存由于某种原因程序未释放/无法释放就叫内存泄漏,这通常是由于没有合理使用闭包、未解除定时器或忘记解除事件监听等引起的。

垃圾回收算法

堆栈空间分配区别

  1. 栈:由操作系统自动分配释放函数的参数值、局部变量等(基本数据类型都放在栈里)。
  2. 堆:一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收(复杂数据类型都放在堆中)。

引用计数法

IE采用的就是这个,定义为内存不再使用。现在不常用,就是看一个对象是否还有指向它的引用,没有引用了就回收该对象。

  • 算法

    1. 用一个引用计数器跟踪记录被引用的次数。
    2. 如果被引用了一次,记录 ++,多次则累加。
    3. 如果减少一个引用就 --。
    4. 如果引用次数 === 0,则释放内存。
  • 优势

    • 实时回收:引用计数可以在对象不再被引用时立即回收,不需要等待垃圾收集器的运行。这可以减少内存占用和提高程序的性能。
    • 简单高效:引用计数是一种简单的垃圾收集算法,实现起来相对容易,不需要复杂的算法和数据结构。
  • 缺点

    • 循环引用:如果存在一个嵌套引用(循环引用),垃圾回收器不会进行回收(因为引用次数永远不为0),导致内存泄漏。
    function fn () {
    let o1 = {}; //地址0001
    let o2 = {}; //地址0002
    o1.a = o2;  //指向0002
    o2.a = o1; //指向0001
    console.log(o1);
    //打印出的o1里的a属性指向o2,o2里的a属性打开又指向o1,如此循环
    
    • 计数开销:维护每个对象的引用计数需要占用额外的内存空间,而且每次添加、删除引用都需要更新计数,增加了额外的开销。

标记清除法

现代浏览器不用引用计数法了,大多数用标记清除法,整体思想都是一致的。 核心:

  1. 标记清除法将不再使用的对象定义为无法达到的对象
  2. 从根部(在js中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,都是还需要使用到的。
  3. 无法由根部出发触及的对象被标记为不再使用,稍后进行回收。

优势

  • 简单有效:标记清除法相对简单,容易实现。它可以准确地找到不再被引用的对象,并回收内存。
  • 处理循环引用:标记清除法能够处理循环引用的情况。当对象之间存在循环引用时,即使它们不再被任何其他对象引用,引用计数法也无法将它们识别为垃圾,而标记清除法可以通过遍历的方式找到并清除这些对象。

缺点:

  • 垃圾回收过程中的停顿:标记清除法会暂停程序的执行,进行垃圾回收操作。当堆中对象较多时,可能会导致明显的停顿,影响用户体验。
  • 内存碎片化:标记清除法会在回收过程中产生大量的不连续的、碎片化的内存空间。这可能导致后续的内存分配难以找到足够大的连续内存块,从而使得内存的利用率降低。

标记整理法

可以看成是清除法的增强版,它解决了清除法导致的内存不连续、碎片化的问题。标记整理法会先执行整理,移动对象位置,对内存空间进行压缩。过程:

  1. 标记阶段:将所有活动对象进行标记。
  2. 整理阶段:将内存中的活动对象移动到一端,使得空闲空间连续,并且没有碎片化。
  3. 清除阶段:将未标记的对象进行清除操作,并回收其占用的内存空间。

优势

  • 解决了标记清除法带来的内存不连续碎片化问题。

缺点

  • 垃圾回收过程中的停顿:还是存在进行垃圾回收操作的时候会暂停程序的执行。当堆中对象较多时,可能会导致明显的停顿,影响用户体验。

闭包

闭包是一个函数和对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域。

简单理解:闭包 = 内层函数 + 外层函数的变量

function outer () {
    let a = 10;
    function inner() {
        console.log(a);
    }
    return inner;
}
let fn = outer();
fn(); //10

上述代码中,outer函数里有一个inner函数,outer函数的返回值是inner,在外面定义了一个fn引用outer函数并调用,控制台输出10,而打印是在inner函数里进行的,这说明inner函数可以拿到outer函数的变量,符合内层函数+外层函数的变量这个解释,此时outer函数就形成了一个闭包。

简写

function outer1() {
    let a = 101;
    return function () {
        console.log(a);
    }
}
let fn1 = outer1();
fn1();

在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,只是大闭包和小闭包的区别。

闭包作用

封闭数据,提供操作,外部也可以访问函数内部的变量,允许将函数与其所操作的某些数据关联起来

闭包应用

实现数据的私有

例如:我们要做一个统计函数调用次数的API,函数调用一次就++

let i = 0;
function fnn() {
    i++;
    console.log(`函数被调用了${i}次`);
}
fnn(); // 函数被调用了1次
fnn(); // 函数被调用了2次
//but i 是全局变量,可以被任何人修改,不安全
i = 7;
fnn(); //函数被调用了8次

上述代码中先调用了两次函数,然后在外界修改了计数器i的值,再次调用函数时计数器就从修改后的值开始 ++,数据可以被随意篡改,这样非常危险。

使用闭包

function counter() {
    let j = 0;
    return function() {
        j++;
        console.log(`函数被调用了${j}次`);
    }
}
const result = counter();
result(); //函数被调用了1次
j = 4;
result();  //函数被调用了2次

使用了闭包后,外界就无法手动修改闭包内的数据。即使在外界修改了计数器的数值,调用函数时输出的函数内部的计数器还是正常计数不受外界干扰。

闭包存在的问题

可能会发生内存泄漏。

function outer1() {
    let a = 101;
    return function () {
        console.log(a);//存在对a的引用
    }
}
let fn1 = outer1();
fn1();

上述代码中,fn1()执行完毕后,outer1会自动销毁,但outer1函数内的a不会被销毁,因为内部return的函数对a有一个一直存在的引用,并且根据垃圾回收机制,被另一个作用域引用的变量不会被回收。

解决方法 fn1() = null;

预编译、作用域、作用域链

预编译

编译是发生在代码执行之前的,编译过程主要是:

  1. 词法分析(词法单元)
  2. 语法解析(抽象语法树)
  3. 代码生成

预编译的情况有三类

  1. 发生在代码执行之前

    • 变量声明提升

      1. 在代码执行前会先去找所有var声明的变量,全部提到当前作用域的最前面。
      2. 只提升声明,不提升赋值。
      <script>
      console.log(a);
          var a = 2;
      </script>
      

      会变成

      <script>
      var a;
      console.log(a);
          a = 2;
      </script>
      

      所以打印的结果为underfined ,而不是报错

    • 函数提升

      1. 函数提升跟变量提升类似,会把所有函数的声明提升到当前作用域的最前面。
      2. 只提升函数声明,不提升函数调用。
      3. 函数表达式不存在提升的现象。
      // 调用函数
      fn();
      // 函数声明
      function fn() {
          console.log('tititi')
      }
      

      相当于

      function fn() {
          console.log('tititi')
      }
      fn();
      

      所以不会报错

      函数表达式是不存在提升的,因为这种创建函数的方式是赋值,而不是声明,赋值是不提升的,所以无法在这之前调用函数。

      var fun = function () {
          console.log('...')
      }
      fun(); // 只能在赋值声明后面调用
      
  2. 发生在函数执行之前

    1. 在找到函数后先创建一个AO对象
    2. 找函数的形参和变量声明,将其作为AO的属性,赋值为undefined(提升)。
    3. 将实参和形参统一。
    4. 在函数体内寻找函数声明,将函数名作为AO对象的属性,赋值为一个空函数{}
  3. 发生在全局

    1. 创建一个GO对象
    2. 找全局变量声明,将其作为GO的属性,赋值为undefined
    3. 找全局函数声明,将函数名作为GO的属性,赋值为{}

作用域

作用域也叫运行上下文,当函数执行时会创建一个称为执行期上下文的内部对象,一个执行期上下文定义了一个函数执行时的环境,函数每次执行时的上下文都是独一无二的,所以多次调用同一个函数都会创建一个独立的执行期上下文,当函数执行完毕这个执行期上下文就会被销毁。

作用域也分为全局作用域局部作用域,局部作用域又分为函数作用域块级作用域{}。ES6带来的letconst声明变量可以提供了块级作用域。

作用域链

执行期上下文对象的集合,这个集合呈链式连接,我们把这种链式连接叫做作用域链

作用域链[[scope]]本质上是底层的变量查找机制

在函数被执行时优先查找当前函数作用域中的变量,如果当前作用域查找不到,就依次逐级查找父级作用域直到全局作用域。

let && const

let和const是es6提供的声明变量和常量的方法,在这之前使用的都是var。使用var会遇到变量提升等问题。

变量提升带来的问题

  1. 变量容易在不被察觉的情况下被覆盖掉。

  2. 本应销毁的变量没有被销毁。 因此es6引入了let和const解决该问题。

let

变量提升

  • var存在变量提升
console.log(a); // underfined
var a = 2;
  • let不存在变量提升
console.log(a) // error
let a = 2;

作用域

  • var是全局作用域
console.log(d); // underfined
if (true) {
    var d = '666';
    console.log(d);
}
console.log(d); //666
  • let是一个局部作用域
    • 局部 1. 函数内 2. 块内{}
console.log(c); // c is not defined
if (true) {
   let c = 1;
   console.log(c);
}
console.log(c); // c is not defiend

所以一般声明变量用let比较严谨。

重复声明&&污染全局变量

  • var可以重复声明变量具有覆盖性
var e = 1;
var e = '666';
console.log(e); //666

在代码量太多的情况下可能会一不小心声明重名的变量,这样很危险。 例如:

//window.RexExp本来是一个函数
var RegExp = 10;
console.log(RegExp); // 10
//全局变量被覆盖
console.log(window.RegExp); // 10
  • let不可以重复声明,不会污染全局变量
let g = 6;
let g = 7;
console.log(g); //Identifier 'g' has already been declared
let RegExp = 10;
console.log(RegExp);// 10
console.log(window.RegExp); // f RegExp() {[native code]}

const

const与let差不多,唯一区别就是const声明的是常量,一旦被声明无法修改。

特点

  1. 不能重复声明。
  2. 没有变量提升。
  3. 块级作用域。
  4. 一旦赋值不能修改。
const t = 1;
t = 3; //Assignment to constant variable.

const声明对象

const 声明对象是指向地址,可以修改内部属性但不能重新赋值为另外一个对象。

const person = {
    name: 'cloud'
}
person.name = 'huan';
console.log(person); // {name: 'huan'}

一道面试题

const arr = [];
for (var i = 0; i < 10; i++) {
    arr[i] = function () {
        return i;
    }
}
console.log(arr[5]()); // 10

var 声明会变量提升且是全局作用域。arr[i]是一个函数,函数返回值是i,是个全局变量,所以i = 10的时候里面用到i的地方也全都是10。

执行顺序:

const arr;
const i;
arr = [];
for (i = 0; i < 10; i++) {
    arr[i] = function () {
        return i;
    }
console.log(arr[5]()); // 10

要看清楚 i 不是作为数组成员,而是作为一个函数的返回值,我一开始看错了就很不理解(

用let声明 i 就不会全是10,因为let是块级作用域,每个function都是独立的一个块,所以循环的10个arr[i]里的i都是独立的。

const arr1 = []
for (let i = 0; i < 10; i++) {
    arr[i] = function () {
        return i;
    }
}
console.log(arr[5]()); //5

函数参数

动态参数arguments

  1. arguments只存在于函数里,是函数内部内置的伪数组变量
  2. 可以动态地获取函数的实参。
  3. 虽然是一个伪数组,但拥有length属性索引属性
  4. 可以通过for循环依次得到传递过来的实参。
function getSum() {
    let sum = 0;
    //遍历伪数组
    for (let i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
}
getSum(1, 2); // 3
getSum(1, 2, 3); // 6

剩余参数...

  1. 剩余参数也应用于未知参数量的函数形参上。

  2. 只存在于函数里

  3. 剩余参数...是语法符号,可以把多个独立的参数合并到一个真实的数组里。

function getSum(...arr) {
    let sum = 0;
    //遍历伪数组
    for (let i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
}
getSum(1, 2); // 3
getSum(1, 2, 3); // 6

如何理解剩余?

function getSum(a, b, ...arr) {
    console.log(arr); // [3, 4, 5]
}
getSum(1, 2, 3, 4, 5); 

展开运算符...

  1. 展开运算符在数组中使用,可以展开数组。
  2. 不修改原数组。
const arr = [1, 2, 3];
console.log(...arr); // 1 2 3

典型应用场景

  1. 求数组最大值(最小值)。
//es5
//apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传
const maxNum = Math.max.apply(null, arr);
console.log(maxNum); // 199

// es6 扩展运算符更方便
const maxNum1 = Math.max(...arr);
console.log(maxNum1); // 199
  1. 合并数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr = [...arr1, ...arr2];
console.log(arr); // [1, 2, 3, 4, 5, 6]
  1. ...

函数的默认值

带默认值的参数如果没有传参会报错:

 function add(a, b) {
    return a + b;
}
console.log(add()); //NAN
  • es5的默认值写法:
function add1(a, b) {
    a = a || 15;
    b = b || 15;
    return a + b;
}
console.log(add1());//30

这样即使忘记传值也不会输出 NAN。

  • es6写法
    • 将默认值写在参数中:
    function add2(a = 15, b = 20) {
        return a + b;
    }
    console.log(add2()); //35
    //传一个值就代替第一个参数
    console.log(add2(1)); //21
    //传两个就正常用法
    console.log(add2(1, 2)); //3
    
    • 默认值也可以是一个函数:
    function add3(a, b = getVal(5)) {
        return a + b;
        }
        function getVal(val) {
            return val + 5;
        }
        //b是一个函数,结果为 10,传入一个参数代替第一个参数就是 a=10
        console.log(add3(10)); //20
    

箭头函数

目的

引入箭头函数的目的是为了有更简短的函数写法不绑定this

//普通
const fn = function () {}
//箭头
const fn = () => {}

特点

  • 箭头函数没有this指向,箭头函数内部的this会通过查找作用域链找到定义该函数的对象/函数所在的作用域,最外层的作用域链是window。
  • 属于表达式函数,不存在函数提升。
  • 只有一个参数的时候可以省略()
const fn = x => {
    return x;
}
  • 只有一行代码时可以省略 {}return
const fn = x => x + x;
  • 可以直接返回一个对象。
const fn = (name) => ({name: name})
console.log(fn('cloud')); // {name: 'cloud'}

使用场景

适用于本身需要匿名函数的地方

  • 利用箭头函数求和
const getSum = (...arr) => {
    let sum = 0;
    //遍历伪数组
    for (let i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
    return sum;
}
const result = getSum(2, 3);
console.log(result); // 5

this

//es5
let page = {
    id: 123,
    init:function () {
        document.addEventListener('click', function (event) {
            console.log(this); // #document
            this.doing(event.type); // this.doing is not a function
        })
    },
    doing: function () {
        console.log(`事件类型${event.type},当前id: ${id}`);
    }
}
page.init();

这里 init 函数里给 document 绑定监听事件,在监听事件里使用 page 对象的 function,但素普通function的this指向调用它的对象,所以这里的 this 指的是 document,但 doing 方法是定义在 page 这个对象上的,所以会报错。

//es6
let page1 = {
    id: 123,
    init:function () {
        document.addEventListener('click', (event) => {
            this.doing(event.type); 
        })
    },
    doing: function () {
        console.log(`事件类型${event.type},当前id: ${this.id}`); // 事件类型click,当前id: 123
    }
}
page1.init();

这里 init 里 document 绑定监听函数是使用箭头函数,箭头函数是没有作用域的,所以里面使用的 this 会去寻找调用这个方法的函数/对象所在的作用域,这里就是 init 所在的作用域,也就是 page1。使用this.doing(eventType)就不会报错。

如果这里 init 也用箭头函数也会报错。因为这样 init 函数也米有作用域,然后往上寻找, init 是page1 定义的,要找 page1 所在的作用域,定义 page1 的是最外层作用域,也就是 window

使用注意

在给dom事件添加回调函数的时候不建议使用箭头函数。

btn.addEventListener('click', () => {
    console.log(this); // window
});
btn.addEventListener('click', function() {
    console.log(this);  // btn
});

解构赋值

  • 解构赋值是对赋值运算符的一种扩展。
  • 针对数组和对象进行操作。

优点:代码书写上简洁。

数组解构

  • 数组解构是将数组的单元值快速批量地赋值给一系列变量的简介语法
//获取大中小值
const arr = [1, 2, 3];
// es5
const min = arr[0];
const middle = arr[1];
const max = arr[2];
console.log(min, middle, max);


// es6
const [min, middle, max] = arr;
console.log(min, middle, max);

典型应用

  • 交换变量
let a = 1;
let b = 2;
// es5
let c = a;
a = b;
b = c;

// es6
[b, a] = [a, b];
console.log(a, b); // 2 1

变量和单元值不相等

  • 变量多,单元值少
const [a, b, c, d] = [1, 2, 3];
console.log(a, b, c, d); // 1 2 3 undefined
  • 变量少,单元值多
const [a, b] = [1, 2, 3];
console.log(a, b); // 1 2 
  • 可以用剩余参数解决变量少,单元值多的问题
const [a, b, ...c] = [1, 2, 3, 4];
console.log(a, b, c); // 1 2 [3, 4]
  • 可以通过默认值防止 undefined 传递
const [a = 0, b = 0] = [];
console.log(a, b); // 0 0
  • 可以按需进行赋值
//逗号占位
const [a, b, , d] = [1, 2, 3, 4];
console.log(a, b, d); // 1 2 4
  • 支持多维数组
const arr = [1, 2, [3, 4]];
//想要拿到 3
// es5
console.log(arr[2][0]); // 3

// es6
//逗号占位
const [, , [c, ]] = arr;
console.log(c);  // 3

对象解构

  • 对象解构是将对象的属性和方法快速批量地赋值给一系列变量的简介语法
let node = {
    type: 'id',
    name: 'cloud',
    job: 'sleep'
};
// es5
let type = node.type;
let name = node.name;
let job = node.job;

// es6 
let {type, name, job} = node;
console.log(type, name, job); // id cloud sleep

以前获取一个对象的属性需要依次定义变量去赋值,使用解构可以直接{属性名} = 对象 去获取属性。

一些用法

  • 给新的变量名进行赋值旧变量名: 新变量名
let node = {
    type: 'id',
    name: 'cloud',
    job: 'sleep'
};
let {type, name: uname, job} = node;
console.log(uname); // cloud
  • 数组对象解构
const cat = [
    {
        name: 'cloud',
        age: 2
    }
]
const [ {name, age} ] = cat;
console.log(name, age); // cloud 2
  • 多级对象解构
const mercury = {
    name: 'スレッタ',
    family: {
        wife: 'ミオリネ',
        mother: 'プロスペラ',
        sister: 'エリクト',
    },
    age: 18
}
const { name, family: { wife, mother, sister } } = mercury;
console.log(name, wife, mother, sister); // スレッタ ミオリネ プロスペラ エリクト
  • 多级数组对象解构
const mercury = [
    {
        name: 'スレッタ',
        family: {
            wife: 'ミオリネ',
            mother: 'プロスペラ',
            sister: 'エリクト',
        },
        age: 18
    }
]
const [{ name, family: { wife, mother, sister } }] = mercury;
console.log(name, wife, mother, sister); // スレッタ ミオリネ プロスペラ エリクト

典型用法

  • 假设我们从后台获取了一些数据

    const msg = {
        "code": 200,
        "msg": "获取新闻列表",
        "data": [
            {
                "id": 1,
                "title": "我好累啊",
                "count": 54
            },
            {
                "id": 2,
                "title": "早八滚出拆那",
                "count": 56
            },
            {
                "id": 3,
                "title": "发明调休的永休了",
                "count": 94
            }
        ]
    }
    
  • 现在我们需要把里面的 data 传递给一个 render 函数,有三种写法

    1. 先解构再传值
      const { data } = msg;
      function render ( data ) {}
      render(data);
      
    2. 先传值再解构
      function render ( msg ) {
          const { data } = msg;
      }
      render(msg);
      
    3. 在传值的过程中解构
      function render ( { data } ) {}
      render(msg);
      
  • 为了防止 msg 里面的 data 名字混淆,要求 render 函数里面的数据名 data 改为 myData

    function render ({data: myData}) {}
    render(msg);
    

对象

创建对象的方法

  1. 字面量
    const obj = {}
    
  2. new Object
    const obj = new Object();
    
  3. 构造函数

内置构造函数

在JS 中最主要的数据类型有六种:

  • 基本数据类型
    • 字符串、数值、布尔、undefined、null
  • 引用类型
    • 对象

但素JS中字符串、数值等基本数据类型也可以使用属性、方法,是因为这些基本类型也都有专门的构造函数,称为包装类型

JS中几乎所有的数据都可以基于构造函数创建。

Object

三个常用的静态方法

静态方法是只有构造函数才可以调用的。

  1. Object.keys(obj)

获取对象的所有属性名并返回一个数组。

const obj = {
    name: 'cloud',
    age: 18,
    sex: 'women'
}
console.log(Object.keys(obj)); //  ['name', 'age', 'sex']
  1. Object.values(obj)

获取对象的所有属性值并返回一个数组。

const obj = {
    name: 'cloud',
    age: 18,
    sex: 'women'
}
console.log(Object.values(obj)); // ['cloud', 18, 'women']
  1. Object.assign(target, obj)

浅拷贝,会把对象拷贝到目标对象上,浅拷贝不会改变原对象。

let ob = {};
const obj1 = {
    name: 'cloud',
    age: 18,
    sex: 'women'
}
const obj2 = {
    hobby: {
        id1: 'swim',
        id2: 'play'
    }
}
console.log(Object.assign(ob, obj1, obj2));

常用于给对象追加新的属性

const obj  = {
    name: 'cloud'
}
Object.assign(obj, age: 18);

Array

常用方法
方法作用说明
forEach遍历数组不返回数组,经常用于查找遍历数组元素\color{blue}{查找遍历数组元素}
filter过滤数组返回新数组\color{blue}{返回新数组},返回的是 筛选满足条件\color{blue}{筛选满足条件}的数组元素
map迭代数组返回新数组\color{blue}{返回新数组},返回的是 处理之后\color{blue}{处理之后}的数组元素,想要使用返回的新数组
reduce累计器返回累计处理的结果,经常用于 求和\color{blue}{求和}
  • reduce

    • arr.reduce(function(上一次的值prev, 当前的值current), 初始值init)
    • 如果没有起始值则第一次循环的 prev 为数组第一个元素的值。
    • 每一次循环,把返回值作为下一次循环的 prev。
    • 如果有初始值,则初始值作为第一次循环的 prev。
    • 所以没有初始值遍历次数会比有初始值少一次。
    const arr = [1, 2, 3, 4];
    const total = arr.reduce(function(prev, current) {
        return prev + current;
    }, 10);
    console.log(total); // 20
    
    // 箭头函数写法
    const total = arr.reduce((prev, current) => prev + current, 10);
    
  • 例:求出一个月薪资总支出

    const arr = [
       {
           name: 'cloud1',
           salary: 10000
       },
       {
           name: 'cloud2',
           salary: 10000
       }, 
       {
           name: 'cloud3',
           salary: 10000
       }
    ]
    let total = arr.reduce((prev, current) => prev + current.salary, 0); // 30000
    
  • map

  • filter every()、some()和filter类型,只不过filter返回的是符合条件的数组,every、some返回的是布尔值,且必须都符合才返回true,而some是一个符合就返回true

  • from()

    将伪数组转为真正的数组

    在这之前可以使用[].slice.call(arguments)将一个具有 length 属性的伪数组转化为真数组:

    function add() {
        let arr = [].slice.call(arguments);
        console.log(arr); //[1, 2, 3]
    }
    add(1, 2, 3)
    
    • 一般将call()方法用于强制指定 this 的指向。
    • arguments 是伪数组不具有slice() 方法,那就意味着 arguments.slice()行不通,所以使用空数组[]去调用 slice() 方法。
    • 通过call()方法强制指定当前函数方法内部变量arguments,返回了函数参数列表(伪数组)。
    • slice()方法将其解析,因为没有给slice参数startend,所以直接返回从0 到最后一个位置的所有参数
    • 将这些参数内部push[]这个创建好的空数组中,这样子就返回了一个数组元素!

使用from

function add() {
let arr1 = Array.from(arguments);
console.log(arr); //[1, 2, 3]
}
add(1, 2, 3);

还可以使用展开运算符

//获取dom中的li元素并转为数组
let lis = document.querySelectorAll('li');
console.log(lis);  //NodeList odeList(4) [li, li, li, li]
console.log([...lis]); //转为真数组 [li, li, li, li]
  • find()

    找出第一个符合条件的值

    let arr = [1, 2, -10, -20, 9, 4]; 
    
    
    let a = arr.find(n => n < 0)
    console.log(a); // -10
    
  • findIndex()

    找出第一个符合条件的索引

    let b = arr.findIndex(n => n < 0);
    console.log(b); // 2
    
  • of()

    将任意类型的一组数据转化为数组

    const arr = [1, 2, 3, 4, 5, [1, 2], { id: 1 }]
    console.log(Array.of(arr)); 
    // [1, 2, 3, 4, 5, Array(2), {…}]
    
  • copywithin

    copywithin(粘贴开始位置,复制开始位置)

    从复制开始位置往后复制到结尾,然后从粘贴开始位置开始粘贴

    console.log([1, 2, 3, 4, 5, 6].copyWithin(0, 3));
    //[4, 5, 6, 4, 5, 6]
    
  • keys()

    取索引值 相当于键值对的键

    let arr = [1, 2, -10, -20, 9, 4];
    for (let index of arr.keys()) {
        console.log(index);
        // 0 1 2 3 4 5 
    }
    
  • values()

    取值

    let arr = [1, 2, -10, -20, 9, 4];
    for (let ele of arr.values()) {
        console.log(ele);
        // 1 2 -10 -20 9 4
    } 
    
  • entries()

    取键值对儿

    let arr = [1, 2, -10];
    for (let [index, ele] of arr.entries()) {
       console.log(index,ele);
       //0 1
       //1 2
       //2 -10
    }
    
    • entries()有一个next() 方法,可以自动往下迭代遍历
    let arr1 = [1, 2, 3];
    let it = arr1.entries();
    console.log(it.next().value); //[0, 1]
    console.log(it.next().value); //[1, 2]
    console.log(it.next().value);//[2, 3]
    console.log(it.next().value);//underfined
    
  • includes()

    表示某个数组是否包含给定的值

    let arr1 = [1, 2, 3];
    console.log(arr1.includes(2)); // true
    console.log(arr1.includes(5)); //false
    

String

常用方法
  • split('分隔符')

    将字符串转化为数组,和join相反(join('分隔符')将数组转为字符串)

    const str = 'pink,red';
    const arr = str.split(',');
    console.log(arr); // ['pink', 'red']
    
  • substring()

    用于字符串截取

    substring()(需要截取的第一个字符的索引indexStart [,结束的索引号indexEnd])

    indexStart开始截取字符,没有indexEnd则默认截取到末尾。有 indexEnd 则截取到indexEnd前一个字符(不包含indexEnd)

    const str = '我好累啊好想躺平';
    console.log(str.substring(5)); // 想躺平
    console.log(str.substring(5,7)); // 想躺
    
  • starstWith()

    判断是否以某字符开头,找到就返回true

    startsWith(检测字符串[, 检测位置索引])

    var str = 'why i am so tired, i want to sleep.';
    console.log(str.startsWith('why i am')); // true
    console.log(str.startsWith('so tired')); //false
    console.log(str.startsWith('so tired', 9)); // true
    
  • includes()

    判断一个字符串是否包含在另一个字符串中,是则返回true区分大小写

    includes(要查找的字符串[,从当前位置开始找,默认0])

    const str = 'why i am so tired, i want to sleep.';
    console.log(str.includes('why')); //true
    console.log(str.includes('want')); // true
    console.log(str.includes('Want')); // false
    

Number

常用方法
  • toFixed()

    保留小数个数

    toFixed('博保留位数')

    const num = 10.923;
    console.log(num.toFixed(2)); // 10.92
    const num = 10;
    console.log(num.toFixed(2)); // 10.00
    

深浅拷贝

将一个对象复制给另一个对象,我们可以使用赋值的方式:

let obj = {
    name: 'cloud',
    age: 18
}
let a = obj;
console.log(a); // {name: 'cloud', age: 18}

不过这个操作是将地址复制给了a对象,所以a对象只是做了个引用。当我们修改a对象里的属性值时,原对象obj里面也会跟着改变,这样非常危险。

a.name = 'miomio';
console.log(obj); // {name: 'miomio', age: 18}

浅拷贝

浅拷贝拷贝的是(一层),若对象/数组里还有复杂数据类型则拷贝的是地址

拷贝对象

  1. 使用展开运算符进行拷贝
    let obj = {
       name: 'cloud',
       age: 18
    }
    const a = {...obj};
    console.log(a); // {name: 'cloud', age: 18}
    a.age = 20;
    console.log(a); // {name: 'cloud', age: 20}
    console.log(obj); // {name: 'cloud', age: 18}
    
    可以看到修改a属性的值,原对象不会受到影响。
  2. 使用assign实现浅拷贝
    const b = Object.assign({}, obj);
    console.log(b); // {name: 'cloud', age: 18}
    b.name = 'miomio';
    console.log(b); // {name: 'miomio', age: 18}
    console.log(obj); // {name: 'cloud', age: 18}
    
  3. 手动实现浅拷贝
    const obj = {
        name: 'sure',
        family: {
            wife: 'miomio'
        }
    }
    const o = {};
    function deepCopy (newObj, oldObj) {
        for (let k in oldObj) {
            //k:属性名 | oldObj[k]:属性值
            //newObj[k] === newObj.k
            newObj[k] = oldObj[k];
        }
    }
    deepCopy(o, obj);
    o.name = 'cloud';
    console.log(o); // {name: 'cloud', family: {…}}
    console.log(obj); // {name: 'sure', family: {…}}
    

拷贝数组

  1. 使用展开运算符实现浅拷贝
    let arr = [1, 2, 3, 4];
    const c = [...arr];
    console.log(c); // [1, 2, 3, 4]
    c[0] = 8;
    console.log(c); // [8, 2, 3, 4]
    console.log(arr); // [1, 2, 3, 4]
    
  2. 使用concat实现浅拷贝
    const d = Array.prototype.concat([], arr);
    console.log(d); // [1, 2, 3, 4]
    d[0] = 6;
    console.log(d); // [6, 2, 3, 4]
    console.log(arr); // [1, 2, 3, 4]
    

但素浅拷贝只能拷贝一层,拷贝对象里的简单数据类型的,对象里存放的复杂数据类型则是引用地址,所以修改复杂数据类型的值会影响原对象。

//对象
const o = obj1;
console.log(o); // {name: 'sure', age: 18, family: {…}}
o.family.wife = 'mio';
console.log(obj1.fmaily.wife); // mio

//数组
let arr1 = [1, 2, [3,4]];
const r = arr1;
console.log(r); // [1, 2, [3, 4]];
r[2][0] = 1;
console.log(arr1[2][0]); // 1

所以浅拷贝只适合单层的对象/数组的拷贝。

深拷贝

深拷贝拷贝的是对象,不是地址。

  1. 通过递归实现深拷贝

    在函数内部调用自己就是函数递归。

    const obj = {
       name: 'sure',
       family: {
           wife: 'miomio'
       },
       hobby: ['eat', 'sleep']
    }
    const o = {};
    function deepCopy(newObj, oldObj) {
       for (let k in oldObj) {
           //处理数组
           if (oldObj[k] instanceof Array) {
               newObj[k] = [];
               deepCopy(newObj[k], oldObj[k]);
           }
           //处理对象
           else if (oldObj[k] instanceof Object) {
               newObj[k] = {};
               deepCopy(newObj[k], oldObj[k]);
           }
           else {
               newObj[k] = oldObj[k];
           }
    
       }
    }
    deepCopy(o, obj);
    console.log(o); // {name: 'sure', family: {…}, hobby: Array(2)}
    o.family.wife = 'mio';
    o.hobby[0] = 'eating';
    console.log(o.family.wife); // mio
    console.log(o.hobby); // ['eating', 'sleep']
    console.log(obj.family.wife); //miomio
    console.log(obj.hobby); // ['eat', 'sleep']
    
    
  2. 通过lodash的clonseDeep实现深拷贝

//引入lodash库
const obj = {
        name: 'sure',
        family: {
            wife: 'miomio'
        },
        hobby: ['eat', 'sleep']
    }
const g = _.cloneDeep(obj);
console.log(g);
g.family.wife = 'mio';
g.hobby[0] = 'eating';
console.log(g.family.wife); // mio
console.log(g.hobby); // ['eating', 'sleep']
console.log(obj.family.wife); //miomio
console.log(obj.hobby); // ['eat', 'sleep']
  1. 通过JSON.stringify()实现深拷贝
const obj = {
        name: 'sure',
        family: {
            wife: 'miomio'
        },
        hobby: ['eat', 'sleep']
    }
let m = JSON.stringify(obj)
console.log(m); // {"name":"sure","family":{"wife":"miomio"},"hobby":["eat","sleep"]}
m = JSON.parse(m);
console.log(m); //{name: 'sure', family: {…}, hobby: Array(2)}

先通过JSON.stringify()js对象转化成json字符串,再通过JSON.parse()json字符串转化为js对象,这样一来就跟原对象毫无关系了,还实现了深拷贝。

异常处理

异常处理是指预估代码执行过程中可能发生的错误,然后最大程度地去避免这个错误发生导致的整个程序无法正常运行的这种现象,了解JS异常处理机制,提高代码的健壮性。

throw抛异常

  • throw抛出异常信息。
  • throw终止代码的运行
  • throw后面跟着错误提示信息。一般与Error配合使用,能够设置更详细的异常信息。
 function fn(x, y) {
    if (!x || !y) {
        // throw  // Uncaught SyntaxError: Illegal newline after throw
        throw new Error('妹有参数传进来'); // Uncaught Error: 妹有参数传进来
    }
    return x + y;
}
fn();

try/catch捕获异常

  • try/catch 可以捕获错误信息(浏览器提供的错误信息)。
  • 可以将容易写错,可能发生错误的代码写在try里。
  • catch捕获异常后不会中断程序的执行,需要加return中断。
  • try 尝试
  • catch() 捕获
  • finally无论对错最后都会执行
<p>123123</p>
...
function fn() {
    try {
        //可能发生错误的代码要写在try里
        const p = document.querySelector('.p') 
        p.style.color = 'red';
    }
    //拦截错误,提示浏览器提供的错误信息,但不会中断程序的执行
    catch(err) {
        console.log(err.message); // Cannot read properties of null (reading 'style')
        //需要加return中断程序
        return
    }
}
fn();
console.log(11); //不加return会执行这行代码

可以与throw配合使用,这样catch就不用写return了。

...
catch(err) {
    console.log(err.message); // Cannot read properties of null (reading 'style')
    throw new Error('选择器写对没?要你有什么用'); // Uncaught Error: 选择器写对没?要你有什么用
    }
...

finally是无论程序对与错,最后都会执行的代码

debugger

在代码中加入debugger后执行,打开开发者模式,执行到该行代码时会自动暂停并跳转到调试界面。

image.png 相当于在代码中打了个断点,这在很长的代码中比较好用,不用一行一行找。

this

this指向

普通函数

  • 普通函数中谁调用,this就指向谁
    console.log(this); // wiindow
    function fn () {
        console.log(this); // this
    }
    window.fn();
    const obj = {
        sayHi() {
            console.log(this); //指向obj {sayHi: f}
        }
    }
    obj.sayHi();
    

箭头函数

  • 箭头函数本身没有this指向,在箭头函数中使用this,this引用的就是最近作用域里的this,它会一层一层往外查找this,直到找到存在this定义的作用域。
    const o = {
        sayGood: () => {
            console.log(this); //window
        }
    }
    o.sayGood();
    
    sayGood使用 this,它所在的作用域 o对象 就失效了,往上找o对象 所在的作用域,就是window

!!大量使用this的作用域中谨慎使用箭头函数!!

改变this指向

  1. call(thisArg, arg1, arg2...)
  • call()可以调用函数,返回值就是函数的返回值。
function fn () {
    console.log(this); // Window 
}
fn.call();
  • call()可以改变this指向。
function fn () {
    console.log(this); // {name: 'cloud'}
}
const obj = {
    name: 'cloud'
}
fn.call(obj); //指向obj
  • call()可以带参数
function fn (x, y) {
    console.log(this); // {name: 'cloud'}
    console.log(x + y); // 7
}
const obj = {
    name: 'cloud'
}
fn.call(obj, 2, 5); //指向obj
  1. apply(thisArg, [Array])
  • apply()可以调用函数,返回值就是函数的返回值。
function fn () {
    console.log(this); // Window 
}
fn.apply();
  • apply()可以改变this指向。
function fn () {
    console.log(this); // {name: 'cloud'}
}
const obj = {
    name: 'cloud'
}
fn.apply(obj); //指向obj
  • apply()可以带数组参数。

可以使用apply求数组最大/小值

const arr = [1, 3, 5, 6, 9];
const max = Math.max.apply(null, arr); // 9
const min = Math.min.apply(null, arr); // 1
  1. bind(thiArg, arg1, arg2...)
  • bind()不会调用函数,返回值是拷贝的调用它的函数,不同的是更改了this指向。
function fn () {
    console.log(this); // {name: 'cloud'}
    
}
const obj = {
    name: 'cloud'
}
const fun =  fn.bind(obj);
console.log(fun);/* ƒ fn(x, y) {
                    console.log(this);
                }*/
fun(); // 调用拷贝的函数

想改变this指向但不想调用函数的时候可以使用bind,比如改变定时器内this的指向。

性能优化

防抖

单位时间内,频繁触发事件,只执行最后一次

使用场景

  1. 搜索框输入:只需用户最后一次输入完,再发送请求。
  2. 检测输入格式:手机号、验证邮箱等,等用户输入完毕再去去检测。
  • 案例:当我们在盒子内移动鼠标,盒子内的数字就+1 image.png
    const box = document.querySelector('.box');
    let i = 1;
    function mouseMove() {
        box.innerHTML = i++;
    }
    box.addEventListener('mousemove', mouseMove);
    
    这个代码只要鼠标在盒子内移动一像素就会执行函数mouseMove,如果里面有大量的耗性能的代码,比如dom操作、数据处理等,可能造成卡顿。此时就可以使用防抖。
  • 鼠标在盒子上移动,鼠标停止500ms之后,里面的数字才会+1
    1. lodash的debounce(func, [wait=0], [opctions=])实现防抖 创建一个debounce(防抖动)函数,该函数会从上一次被调用后,延迟wait毫秒后调用func函数。
      const box = document.querySelector('.box');
      let i = 1;
      function mouseMove() {
          box.innerHTML = i++;
      }
      box.addEventListener('mousemove', _.debounce(mouseMove, 500));
      
    2. 手写防抖
      • 声明一个定时器变量。
      • 当鼠标每次滑动都先判断有咩有定时器,如有有定时器先清除以前的定时器。
      • 如果没有定时器则开启定时器,存到变量里。
      • 在定时器内调用要执行的函数。
      const box = document.querySelector('.box');
      let i = 1;
      function mouseMove() {
          box.innerHTML = i++;
      }
      function debounce(fn, time) {
          let timer = null;
          return function () {
              if (timer) clearTimeout(timer);
              timer = setTimeout(() => {
                  fn();
              }, time);
          }
      }
      //这里面的debounce带了括号debounce()是在调用函数,所以只会执行一次,
      //要在这个函数里面return一个函数,就能反复执行了,相当于debounce().function(){}
      box.addEventListener('mousemove', debounce(mouseMove, 500));
      

节流

单位时间内,频繁触发时间,只执行一次。

使用场景

  1. 高频事件:鼠标移动movemouse、页面尺寸缩放resize、滚动条滚动scroll...
  • 案例:鼠标在盒子上移动,不管移动多少次,只要鼠标还在盒子内移动,就每隔500ms +1。
    1. lodash的throttle(func, [wait=0], [options=])实现节流 创建一个throttle(节流)函数,该函数提供一个cancel方法取消延迟的函数调用以及flush方法立即调用。
      const box = document.querySelector('.box');
      let i = 1;
      function mouseMove() {
          box.innerHTML = i++;
      }
      box.addEventListener('mousemove', _.throttle(mouseMove, 500));
      
    2. 手写节流函数
      • 声明一个定时器变量。
      • 当鼠标每次滑动都先判断有咩有定时器,如有有定时器则不开启新定时器。
      • 如果没有定时器则开启定时器,存到变量里。
      • 在定时器内调用要执行的函数。
      • 定时器里面要清除定时器。
      const box = document.querySelector('.box');
      let i = 1;
      function mouseMove() {
          box.innerHTML = i++;
      }
      function throttle(fn, time) {
          let timer = null;
          return function () {
              if (!timer) {
                  timer = setTimeout(() => {
                      fn();
                      //在定时器里无法清除定时器,所以设为null
                      timer = null;
                  }, time);
              }
          }
      }
      box.addEventListener('mousemove', throttle(mouseMove, 500));