# 数据类型√

# JS数据类型√

  • 原始类型8种:number、string、boolean、null、undefined、
  • ES6添加的:symbol、bigInt
  • 引用类型:Object、Array、函数

栈存放指针、堆存放实体

# 判断数据类型的办法√

  • typeof 判断基本数据类型,和函数,不能准确判断null
  • instanceof 判断对象类型,依据原型链判断
  • Object.prototype.toString.call() 返回调用者的具体类型,所有基础类型都能判断,对象数组也可以

# 0.1+0.2为什么不等于0.3

进制转换:IEEE754,最大存储53为有效数字,大于的会被截掉导致精度丢失

解决办法:转为大数或整数运算

# new操作符具体做了什么 √

function myNew(context) {
  // 创建一个新的对象
  const obj = new Object();
  // 设置空对象的__proto__为构造函数的prototype
  obj.__proto__ = context.prototype;
  // 构造函数的this指向这个对象,执行构造函数的代码
  const res = context.apply(obj, [...arguments].slice(1));
  // 判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象
  return typeof res === "object" ? res : obj;
}

# dom.onclick和dom.addEventListener区别?√

dom.onclick:元素被点击是发生

  • 可以在内联标签上添加事件,只能有一个,后来的会覆盖前者
  • 潜在安全问题XSS跨站脚本攻击
  • 内联不能分离文档结构和逻辑
  • 只能处理冒泡阶段

dom.addEventListener:用于向元素添加事件,三个参数分别是事件、回调函数、是否在捕获和冒泡阶段执行

  • 可以添加多个事件,按注册顺序执行
  • 分离文档结构和逻辑
  • 可以处理冒泡和捕获阶段,true表示在捕获阶段处理,false表示在冒泡阶段处理

# 作用域和闭包√

# 作用域

作用域:代码的当前上下文,控制着变量和函数的可见性和生命周期。最大的作用是隔离变量,不同作用域下同名变量不会冲突。

作用域链:当前作用域没有查到值,会向上级作用域查询,直到全局作用域。

作用域分类:

  • 全局作用域:代码在程序的任何地方都能被访问,例如window对象。生命周期和页面生命周期等同。

  • 模块作用域:AMD、CommonJS、ES6等等。模块有自己独立的作用域

  • 函数作用域:闭包在该作用域产生

  • 块级作用域:JS的变量提升存在变量覆盖、变量污染的情况,ES6引入了块级作用域let、const

# 执行上下文(重点)

执行一段代码时的准备工作,成为执行上下文,用执行上下文栈管理执行上下文

每个执行上下文,有三个重要属性:

  • 变量对象VO,是执行上下文相关的数据作用域,存储了上下文中定义的变量和函数声明
  • 作用域链[[scope]],活动对象AO保存在作用域链的最前端
  • this

函数执行时上下文的初始化:(注意:父函数执行时,子函数才会被创建)

  • 复制函数[[scope]]属性创建作用域链
  • 用arguments创建活动对象AO
  • 初始化AO,加入形参、函数声明、变量声明
  • 将AO压入函数作用域链的顶端
  • 子函数被创建,保存作用域链到函数的内部属性[[scope]]

# 全局上下文

全局上下文的变量对象就是全局对象

# 函数上下文

在函数上下文中,用活动对象AO表示变量对象。当进入一个执行上下文中,这个上下文的变量对象才会被激活,所以叫活动对象。通过函数的arguments属性初始化。

进入执行上下文时,会先处理函数声明,在处理变量声明,变量对象包括:

  • 函数的所有形参
  • 函数声明
  • 变量声明

代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值

function foo() {
    console.log(a);
    a = 1;
}

foo(); // Uncaught ReferenceError: a is not defined

function bar() {
    a = 1;  //a会被绑定在全局作用域
    console.log(a);
}
bar(); // 1
console.log(foo); // 函数,因为会优先处理函数声明

function foo(){
    console.log("foo");
}

var foo = 1;
# 函数创建(复制)

函数创建时,就会把所有的父变量对象保存在内部属性[[scope]]中,此时不包含自身的变量对象

# 函数激活(添加)
Scope = [AO].concat([[Scope]]); 

函数激活时,进入函数上下文,创建VO/AO之后,就会将活动对象添加到作用域链的前端

# 作用域链

查找变量时,会先从当前上下文的变量对象中查找,没有找到,会从父级执行上下文查找,直到查找全局上下文。

# this

Reference,是一种规范中的类型,用于描述语言底层行为逻辑

var foo = 1;

// 对应的Reference是:
var fooReference = {
    base: EnvironmentRecord,// 属性所在的对象
    name: 'foo',//属性的名称
    strict: false
};

获取Reference组成部分的方法:

  • GetBase:返回reference的base value
  • IsPropertyReference:如果base value是一个对象,就返回true
  • GetValue:返回具体的值

函数调用:

  1. 计算MemberExpreesion,结果赋给ref
  2. 判断ref是不是Reference类型,如果是:
    1. 如果IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
    2. 或者base value 是Environment Record,那么那么this的值为 ImplicitThisValue(ref)
  3. 否则,this为undefined

MemberExpreesion是什么?简单理解就是表达式中()左边的部分

var value = 1;

var foo = {
  value: 2,
  bar: function () {
    return this.value;
  }
}

//示例1 foo.bar this->foo
console.log(foo.bar());
//示例2 (foo.bar)和foo.bar一样 this->foo
console.log((foo.bar)());
//示例3   GetValue this->undefined,会被隐式转换为全局对象
console.log((foo.bar = foo.bar)());
//示例4   GetValue this->undefined
console.log((false || foo.bar)());
//示例5   GetValue this->undefined
console.log((foo.bar, foo.bar)());

# 闭包

# 理解闭包

表象:函数A包含了函数B,函数B使用了函数A的变量,那么函数B就被称为闭包,闭包就是能够 读取函数A内部变量的函数

实质:在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的[[scope]]中,形成作用域链,所以即使父级函数的执行上下文销毁,但是活动对象还是存储在内存中,可以被内部函数访问到,所以实现了闭包。

闭包的特征:

  • 函数中存在函数
  • 内部函数可以访问外部函数的额作用域
  • 外部函数的参数和变量不会被GC,始终留在内存中
  • 使用闭包会消耗内存

# 应用场景

  • for循环闭包变量i
// demo1 输出 3 3 3  var定义的是全局变量
for(var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
} 
// demo2 输出 0 1 2  let具有块级作用域
for(let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}
// demo3 输出 0 1 2  闭包
for(let i = 0; i < 3; i++) {
    (function(i){
        setTimeout(function() {
        console.log(i);
        }, 1000);
    })(i)
}
  • 模拟私有方法
/* 模拟私有方法 */
// 模拟对象的get与set方法
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
    privateCounter += val;
}
return {
    increment: function() {
    changeBy(1);
    },
    decrement: function() {
    changeBy(-1);
    },
    value: function() {
    return privateCounter;
    }
}
})();
  • setTimeout中使用
// setTimeout(fn, number): fn 是不能带参数的。使用闭包绑定一个上下文可以在闭包中获取这个上下文的数据。
function func(param){ return function(){ alert(param) }}
const f1 = func(1);
setTimeout(f1,1000);
  • 实现继承
// 以下两种方式都可以实现继承,但是闭包方式每次构造器都会被调用且重新赋值一次所以,所以实现继承原型优于闭包
// 闭包
function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

# 预解析和变量提升 √

预解析:JS引擎将JS所有var声明和function都提升到作用域的最前面

变量提升:将所有的变量声明提升到作用域的最前面

# call、apply、bind √

call和apply是使用后马上执行,bind是返回一个新的函数

call与apply的区别在于传参,call需要把参数列举出了

call实现:

Function.prototype.myCall = function (context) {
  // 判断调用对象
  if (typeof this !== "function") {
    throw new Error("Type error");
  }
  // 首先获取参数
  let args = [...arguments].slice(1);
  let result = null;
  // 判断 context 是否传入,如果没有传就设置为 window
  context = context || window;
  // 将被调用的方法设置为 context 的属性
  // this 即为我们要调用的方法
  context.fn = this;
  // 执行要被调用的方法
  result = context.fn(...args);
  // 删除手动增加的属性方法
  delete context.fn;
  // 将执行结果返回
  return result;
};

apply的实现:

Function.prototype.myApply = function (context) {
  if (typeof this !== "function") {
    throw new Error("Type error");
  }
  let result = null;
  context = context || window;
  // 与上面代码相比,我们使用 Symbol 来保证属性唯一
  // 也就是保证不会重写用户自己原来定义在 context 中的同名属性
  const fnSymbol = Symbol();
  context[fnSymbol] = this;
  // 执行要被调用的方法
  if (arguments[1]) {
    result = context[fnSymbol](...arguments[1]);
  } else {
    result = context[fnSymbol]();
  }
  delete context[fnSymbol];
  return result;
};

bind实现:

Function.prototype.myBind = function (context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new Error("Type error");
  }
  // 获取参数
  const args = [...arguments].slice(1),
  const fn = this;
  return function Fn() {
    return fn.apply(
      this instanceof Fn ? this : context,
      // 当前的这个 arguments 是指 Fn 的参数
      args.concat(...arguments)
    );
  };
};

# ...args剩余参数和arguments对象的区别

  • ...args只包含没有形参的实参,而arguments包含了传给函数的所有实参
  • ...args是真正的Array实例,arguments不是真正的数组
  • arguments还有附加属性callee

# 遍历方法

# 遍历数组

  • for循环
  • forEach() 不能执行break和return
  • map()
  • for...of 可迭代的对象都可遍历

# 遍历对象

  • for...in 遍历一个对象自由的、继承的、可枚举的、非Symbol的属性名
  • Object.keys() 返回一个可枚举属性名组成的数组
  • Object.values() 可枚举属性值
  • Object.getOwnPropertyNames() 返回指定对象的所有自身属性名

# 遍历字符串

  • for...of
  • for循环

# JS的NAN属性

NaN属性代表非数字值的特殊值,不可配置,不可写

NaN!==NaN

isNaN()判断是否为NaN

# null和undefined

undefined == null //true
undefined === null //false
let a
typeof a //undefined
let b= null
typeof b //object
null表示空对象指针,JS的最初版本使用32位系统,为了性能考虑使用低位存储变量类型信息,000开头代表对象,而null表示为全零

# ===和==

==对比时,若类型不相同,会先转换为相同类型,然后再比较值

  • 一个是null、一个是undefined、相等
  • 一个是字符串、一个是数值、字符串转换为数值
  • true/false转换为1/0
  • 一个是对象、一个是数值和字符串
    • 先尝试valueOf()
    • toString()

===不会转换类型

# 函数的length√

  • 形参的个数
function fn1 () {}
function fn2 (name) {}
function fn3 (name, age) {}

console.log(fn1.length) // 0 
console.log(fn2.length) // 1 
console.log(fn3.length) // 2
  • 第一个具有默认值之前的参数个数
function fn1 (name) {} 
function fn2 (name = '林三心') {} 
function fn3 (name, age = 22) {} 
function fn4 (name, aaa, age = 22, gender) {} 
function fn5(name = '林三心', age, gender, aaa) {} 

console.log(fn1.length) // 1 
console.log(fn2.length) // 0 
console.log(fn3.length) // 1 
console.log(fn4.length) // 2
console.log(fn5.length) // 0

# DOM事件流√

事件三要素:事件源、绑定事件、添加事件处理程序

事件流:

  • 捕获阶段:DOM最顶层window开始,逐级向下寻找目标元素传播的过程,遇见绑定的捕获事件会向下传递
  • 目标阶段:到达目标阶段出发绑定事件
  • 冒泡阶段:事件开始由具体元素接受,逐级向上传播到DOM顶层window的过程

有了事件流,就可以控制事件在事件流的那一个环节执行

# 中文的长度

js是unicode编码,所有的字符都是一个

前后端不一致的情况,可以用正则替换后再获取长度

# 简述cookie/session记录登录状态机制原理

第一次访问服务器,服务器会开辟一块空间来存放用户信息。

每一个登录之后的用户信息,都会以key-value格式记录在session中

同事服务器会把sessionId存在cookie中返回给客户端

客户端把sessionId保存在本地cookie中对应的网站记录下

下次访问携带这个sessionId

# 简述cookies、session、sessionStorage、localStorage区别

cookie

  • 把数据存在用户的浏览器中
  • 刷新页面不会丢失数据
  • 只能存储字符串

session

  • 位于web服务器,主要负责访问者与网站的交互,关闭网站时会话就已经结束

sessionStorage

  • 生命周期为关闭浏览器
  • 同一个窗口数据可共享
  • 2.5m

localStorage

  • 生命周期为永久
  • 多个窗口共享
  • 5m

cookie与sessionStorage、localStorage区别:

  • cookie始终在同源的http请求中携带,另外两个不会自动发送到服务器
  • cookie不能超过4k,另外两个可到5M以上
  • 数据有效期不同:sessionStorage仅在浏览器窗口关闭之前有效,localStorage始终有效,窗口或浏览器关闭也一直保存;cookie在过期时间之前有效
  • 作用域不同:sessionStorage在同一个的浏览器窗口共享,localStorage和cookie在所有同源窗口共享

# for-in与for-of区别 √

  • for-in:用于遍历对象可枚举属性,包括对象的原型链中的属性,遍历数组的索引
  • for-of:用于遍历可迭代对象,不会遍历原型,遍历数组的值,对象部署了Symbol.iterator属性就可以遍历

# for...of与Iterator

遍历器Iterator提供一种接口,为不同的数据结构提供统一的访问机制,即for...of循环

手写next方法

function makeIterator(array){
  var nextIndex = 0
  return {
    next: function(){
      return nextIndex < array.length ?
        {value:array[nextIndex++],done:false}:
        {done:true}
    }
  }
}
var iter = makeIterator([1,2,3])
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())

一个数据结构只要具有Symbol.iterator属性,就认为是可以遍历的。Symbol.iterator属性本身是一个函数,执行这个函数,返回一个遍历器。

对象没有默认部署Iterator接口

调用了Iterator接口的场合:

  • for...of
  • Array.from()
  • Map(),Set()
  • Promise.all()
  • Promise.race()

# 常见的事件

  • 点击事件:onclick、ondblclick
  • 焦点事件:onblur失去焦点、onfocus获得焦点
  • 加载事件:onload
  • 鼠标事件:onmosedown、onmouseup
  • 键盘事件:onkeydown
  • 选择和改变:onchange
  • 表单事件:onsubmit

# JS严格模式

"use strict" 减少优于隐式转换、全局变量污染带来的安全问题

  • 变量必须声明再使用
  • 预编译时this为undefined,
  • 不支持arguments、caller、callee、with
  • 函数内部不能通过arguments修改参数值
  • 不允许对变量或函数使用delete
  • 不允许变量重名
  • 不允许使用8进制
  • 不可对对象的只读属性赋值
  • 全局this不再指向window对象

# ~~字符√

~是按位取反,~~会保持原值,并转化为int类型

正值相当于Math.floor(x)

# Map和Object区别

  • 内存占用:同样的内存,map比object多存储50%的键值对
  • 插入性能:有大量插入操作时map性能更好
  • 查找速度:大数据量时差异较小,某些情况Object更好
  • 删除性能:选择map

# 位运算符√

位与&、位或|、位异或^、位非~

左移<<、右移>>、无符号右移>>>、无符号左移<<<

# delete删除性能

  • 对象不存在也返回true,不能通过结果来判断
  • 对象的属性存在并且不能删除时返回false
  • 全局作用域或者var、function声明的无法用delete删除

# 浏览器缓存各个文件的具体缓存配置

Content-Length加Last-modified的哈希

# val、let、const

var:

  • 声明提升
  • 同一个作用域可以多次声明,最开始的会被覆盖
  • 没有块级作用域
  • 会绑定在顶层window对象上,其他两个不会

const:

  • 不可重新赋值
  • 引用类型例外
  • 必须同时初始化和赋值
  • 栈保存引用、堆保存值

let:

  • 不会声明提升
  • 可更改
  • 不可重复声明

# 将值设置不可修改

Object.freeze:

  • 不能添加删除属性
  • 不能修改已有属性
  • 对象原型也不能修改

Object.seal:

  • 不能添加删除
  • 可以修改已有属性

Object. preventExtensions:

  • 不能添加新属性

# 箭头函数和普通函数√

箭头函数:

  • 没有prototype属性,不能作为构造函数使用
  • 不能绑定arguments,会继承上一级作用域的arguments
  • this指向定义时父级作用域的this

主要解决问题:

  • 简化函数写法,多用于回调函数
  • 解决this指向问题,函数不再是一等公民

不能通过call、apply、bind绑定

# symbol类型√

定义一个唯一的对象属性名,不可改变。当使用一个别人提供的对象时,想要添加一些属性,可能会重复,那么就可以使用Symbol

  • 作为属性名,解决全局变量名冲突的问题
  • 不能用new构建 var s1 = Symbol("dish")
  • 作为属性key,只能通过括号获取,不能用点获取
  • 不能被for-in迭代

Symbol.description可以读取描述信息

symbol.for()返回的可以是相等的

Symbol.for("bar") === Symbol.for("bar")
// true

Symbol("bar") === Symbol("bar")
// false

# 异步√

调用栈call stack,也就是执行上下文栈

​ 异步任务的执行,首先会进入调用栈,解释器会将其响应的回调任务放在任务队列中,紧接着调用栈会删除这个任务。当主线程清空后,解释器会读取任务队列,并依次将已完成的异步任务加入调用栈执行。

# event loop、宏任务和微任务

宏任务(浏览器规定的):整体代码块,setTimeout、setInterval、MessageChannel、IO操作、事件队列、setImmediate(Node环境)

微任务(ES6语法规定的):Promise.[then|catch|finally]、MutationObserver(浏览器环境)、process.nextTick(Node环境)

区别:

  • 宏任务在DOM渲染后触发
  • 微任务在DOM渲染前触发

事件循环的具体流程:

  1. 从宏任务队列中,按照入队顺序,找到第一个执行的宏任务,放入调用栈,开始执行
  2. 执行完该宏任务下所有的同步任务后,该宏任务被推出宏任务队列,然后微任务开始按照入队顺序,依次执行,直到微任务队列清空,微任务中产生微任务会立即添加在队尾
  3. 微任务队列清空后,一个事件循环结束
  4. 接着从宏任务队列,找到下一个宏任务,开始第二轮循环。

# async/await

async将一个同步函数变为异步函数,返回值变为promise

await可以放在任何异步的、基于promise的函数之前,执行过程中会暂停在该代码上,直到promise完成。

async函数的函数体可以被看做是0个或者多个await表达式分开的,从函数的第一行开始到第一个await都是同步运行的。因此,如果async函数有一个await表达式,那么async函数就会异步执行

# promise对象

三个状态:

  • Pending :初始状态,表示异步操作还在进行中
  • Fulfilled:表示异步操作成功,此时会执行.then()方法的回调函数
  • Rejected:异步操作失败,执行.catch()方法的回调函数

创造一个Promise实例

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别为resolve和reject,它们是两个函数,由JS提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从pending变为resolved,在异步函数操作成功时调用,并将异步操作的结果作为参数传递出去;reject函数的作用是,将Promise对象的状态从pending转为rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成后,可以用then方法分别指定resolved状态和rejected状态的回调函数

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数

# Promise.then()

作用是为Promise实例添加状态改变后的回调函数。返回一个新的Promise

# Promise.catch()

相当于.then(null,rejection)或者.then(undefined,rejection)的别名。

这三种写法是等价的:

const promise = new Promise(function(resolve, reject) {
  throw new Error('test');
});
promise.catch(function(error) {
  console.log(error);
});
// Error: test

// 写法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// 写法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});

Promise对象的错误具有冒泡性质,会一直向后传递,直到被捕获为止。所以只需要在链式的尾部添加一个catch即可,通常不要在then里面处理reject

# Promise.finally() ES2018

不管promise对象最后状态如何,都会执行的操作,因为都用的resolve

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

# Promise.all()

用于将多个Promise实例,包装为一个新的Promise实例

const p = Promise.all([p1, p2, p3]);

p的状态由p1、p2、p3决定:

  • 只有p1、p2、p3的状态都变为fulfilled,p->fulfilled,返回数组
  • 只有有一个被rejected,p->rejected,返回第一个被reject的实例的返回值
  • 如果作为参数的p。定义了自己的catch方法,一旦被rejected,并不会触发Promise.all()d的catch方法

# Promise.reslove()

将现有的对象转为Promise对象

参数的情况:

  • 参数是一个Promise实例,返回这个实例
  • 参数是thenable对象,转为Promise对象后,立即执行它的then方法
  • 参数不是thenable对象或者根本不是对象,返回一个新的对象,状态为resolved
  • 不带任何参数,返回一个resolved状态的Promise对象

# Promise.reject()

返回一个新的 Promise 实例,该实例的状态为rejected

# arguments对象√

  • callee用于匿名函数递归
  • 本身不是数组,但有length属性,可以索引元素
  • 检测参数个数
  • 模拟函数重载

# ArrayBuffer

操作二进制数据的一个接口

  • ArrayBuffer代表原始二进制数据
    • TypedArray 读写简单的二进制数据
    • DataView读写复杂二进制数据

# 浏览器工作原理和V8引擎

# 浏览器内核

实质上是浏览器的排版引擎、页面渲染引擎、样板引擎

  • webkit :苹果基于KHTML开发的,用于safari
  • Blink:webkit一个分支,用于Google、edge等

# 浏览器渲染过程√

# 生成渲染树

  1. 解析HTML文件,构建DOM树
  2. CSS解析,构建CSSOM树
  3. 合并形成一个渲染树 Render tree
  4. Layout回流,确定节点在屏幕中的位置和大小
  5. Painting:根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  6. Display:像素发送给GPU,展示在页面上

# 回流

构造渲染树时,需要将可见的DOM及其样式结合起来,还需要计算在视口内的确切位置和大小。计算的阶段就是回流。

# 重绘

将渲染树的每个节点转换为屏幕的实际像素

# v8引擎

  • JS代码、词法语法分析、抽象语法树、解释为字节码、运行结果
  • 上下文栈、预解析、变量提升、作用域、暂时性死区
  • 基本类型存放在栈中分配
  • 复杂类型存放在堆中,引用的指针在栈中

# 垃圾回收机制√

两种策略:

  1. 标记清除:标记阶段为所有活动对象做标记,清除阶段把没有标记的销毁。
    • 缺点:内存碎片化、分配速度慢
    • 采用标记整理算法
  2. 引用计数:如果没有引用指向该对象,对象将被垃圾回收机制回收
    • 缺点:需要一个计数器,所占内存空间大,解决不了循环引用的问题

v8的垃圾回收机制基于标记清除

# 常见手写

# 防抖

事件被触发时,设置一个定时器。如果在定时器内事件再次触发,那么久清除之前的定时器,重新设置一个新的定时器。只有当定时器时间到达之后,才会真正的触发事件。可以保证,事件被频繁触发的时候,只有最后一次会被执行。

# 节流

对于高频率触发的事件,限制触发的时间间隔,以减少对系统资源的消耗

Last Updated: 12/23/2024, 4:18:13 AM