Skip to content
On this page

JavaScript

数据类型

基本数据类型

Number、String、Boolean、Null、Undefined、Symbol、bigInt

  • Symbol是ES6新出的一种数据类型,没有重复的数据,可以作为object的key
  • BigInt也是ES6新出的数据类型,特点是数据涵盖的范围大,能够解决超出普通数据类型范围报错的问题
  • null返回的是一个空的对象
  • typeof检测变量数据类型,数组、对象、null都会被判断为object
  • instanceof只能判断引用数据类型
  • undefined代表未定义,null代表空对象;使用typeof判断时,undefined为undefined,null为object

引用数据类型

object、Array、Date、Function、RegExp

二者区别

  • 简单数据类型:是放在栈里面,里面直接开辟一个空间,存放的是值
  • 复杂数据类型:首先在栈里面存放地址,十六进制表示,然后这个地址指向堆里面的数据

补充点

  • JS是一种弱类型或者说动态语言,这意味着不用提前声明变量的类型,在程序运行过程中类型会被自动确定。JS的变量数据类型是只有程序在运行过程中,根据等号右边的值来确定的。

==和===的区别

===叫做严格相等,是指:左右两边不仅值要相等,类型也要相等;==不像===那样严格,对于一般情况,只要值相等,就返回true,但==还涉及一些类型转换,它的转换规则如下:

  • 两边的类型是否相同,相同的话就比较值的大小,例如1==2,返回false
  • 判断的是否是null和undefined,是的话就返回true
  • 判断的类型是否是String和Number,是的话,把String类型转换成Number,再进行比较
  • 判断其中一方是否是Boolean,是的话就把Boolean转换成Number,再进行比较
  • 如果其中一方为Object,且另一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较

数组常用方法

操作方法

前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响。

  • push()

接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度。

js
let colors = []; // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
console.log(count) // 2
  • unshift()

在数组开头添加任意多个值,然后返回新的数组长度。

js
let colors = new Array(); // 创建一个数组
let count = colors.unshift("red", "green"); // 从数组开头推入两项
alert(count); // 2
  • splice()

传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组。

js
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 0, "yellow", "orange")
console.log(colors) // red,yellow,orange,green,blue
console.log(removed) // []
  • concat()

首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组。

js
let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green","blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]

  • pop()

用于删除数组的最后一项,同时减少数组的length值,返回被删除的项。

js
let colors = ["red", "green"]
let item = colors.pop(); // 取得最后一项
console.log(item) // green
console.log(colors.length) // 1
  • shift()

用于删除数组的第一项,同时减少数组的length值,返回被删除的项。

js
let colors = ["red", "green"]
let item = colors.shift(); // 取得第一项
console.log(item) // red
console.log(colors.length) // 1
  • splice()

传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组。

js
let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第一项
console.log(colors); // green,blue
console.log(removed); // red,只有一个元素的数组
  • slice()

用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组。

js
let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
console.log(colors)   // red,green,blue,yellow,purple
console.log(colors2); // green,blue,yellow,purple
console.log(colors3); // green,blue,yellow

查找元素,返回元素坐标或者元素值。

  • indexOf()

返回要查找的元素在数组中的位置,如果没找到则返回 -1。

js
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.indexOf(4) // 3
  • includes()

返回要查找的元素在数组中的位置,找到返回true,否则false。

js
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.includes(4) // true
  • find()

返回第一个匹配的元素。

js
const people = [
    {
        name: "Matt",
        age: 27
    },
    {
        name: "Nicholas",
        age: 29
    }
];
people.find((element, index, array) => element.age < 28) // // {name: "Matt", age: 27}

  • splice()

排序方法

  • reverse()

将数组元素方向反转。

js
let values = [1, 2, 3, 4, 5];
values.reverse();
alert(values); // 5,4,3,2,1
  • sort()

接受一个比较函数,用于判断哪个值应该排在前面。

js
function compare(value1, value2) {
    if (value1 < value2) {
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); // 0,1,5,10,15

转换方法

  • join()

接收一个参数,即字符串分隔符,返回包含所有项的字符串。

js
let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue

迭代方法

  • some()

对数组每一项都运行传入的测试函数,如果至少有1个元素返回 true ,则这个方法返回 true。

js
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let someResult = numbers.some((item, index, array) => item > 2);
console.log(someResult) // true
  • every()

对数组每一项都运行传入的测试函数,如果所有元素都返回 true ,则这个方法返回 true。

js
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every((item, index, array) => item > 2);
console.log(everyResult) // false
  • forEach()

对数组每一项都运行传入的函数,没有返回值。

js
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
    // 执行某些操作
});
  • filter()

对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回。

js
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
console.log(filterResult); // 3,4,5,4,3
  • map()

对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组。

js
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map((item, index, array) => item * 2);
console.log(mapResult) // 2,4,6,8,10,8,6,4,2

数组去重

  • ES6的Set去重
js
//from搭配Set
Array.from(new Set(arr))
//扩展字符串搭配Set
[...new Set(arr)]
  • ES5的for嵌套for然后splice去重
  • indexOf去重
js
function unique(arr) {
    let newArr = [];
    for(let i = 0; i < arr.length; i++){
        if(newArr.indexOf(arr[i]) === -1) {
            newArr.push(arr[i]);
        }
    }
    return newArr;
}
let demo = unique(['a', 'c', 'z', 'a', 'x', 'c', 'b'])
console.log(demo); //[ 'a', 'c', 'z', 'x', 'b' ]
  • sort()
  • filter()
  • Map()

作用域

作用域分为全局作用域和局部作用域。

全局作用域

script标签和.js文件的最外层就是全局作用域,在此声明的变量在函数内部也可以被访问。全局作用域中声明的变量,任何其他作用域都可以被访问。

局部作用域

局部作用域分为函数作用域和块级作用域。

  • 函数作用域:在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。
  • 块作用域:在JS中使用{}包裹的代码称为代码块,代码块内部声明的变量外部将有可能无法被访问。
js
for(let t = 1; t <= 6; t++){
    //t只能在改代码中被访问
    console.log(t) //正常
}
//超出了t的作用域
console.log(t) //报错

注意:

  • let声明的变量会产生块作用域,var不会产生块作用域
  • const声明的常量也会产生块作用域
  • 不同代码块之间的变量无法相互访问
  • 推荐使用let或者const

作用域链

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

  • 作用域:规定了当前作用域中的变量和函数可被作用的范围
  • 变量的作用域:全局变量(在函数外部定义的变量),局部变量(在函数内部定义的变量)
  • 作用域链:根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问,就称为作用域链
  • 在函数被执行时,会优先查找当前函数作用域中的变量;如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域
  • 子作用域能够访问父作用域,父级作用域无法访问子级作用域
js
function f1(){
    var num = 123;
    function f2({
        console.log(num); //123 (站在目标出发,一层一层地往外查找)
    }
    f2();
}
var num = 456;
f1();

垃圾回收机制(GC)

JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。

内存的生命周期

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

  • 1.内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
  • 2.内存使用:即读写内存,也就是使用变量、函数等
  • 3.内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存

注意:

  • 全局变量一般不会回收(关闭页面回收)
  • 一般情况下局部变量的值不用了会被自动回收掉

内存泄漏

程序中分配的内存由于某种原因程序未释放或无法释放叫做内存泄漏。

浏览器垃圾回收算法

堆栈空间分配区别:

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

引用计数法

IE采用的引用计数算法,定义“内存不再使用”,就是看一个对象是否有指向它的引用,没有引用了就回收对象。

算法:

  • 1.跟踪记录被引用的次数
  • 2.如果被引用了一次,那么就记录次数是1,多次引用会累加++
  • 3.如果减少一个引用就减1--
  • 4.如果引用次数是0,则释放内存

存在的问题:嵌套引用(循环引用)。如果两个对象相互引用,尽管他们已不再使用,垃圾回收机制不会进行回收,导致内存泄漏。

标记清除法

核心:

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

预解析

JS引擎运行JS分为两步:预解析、代码执行。

  • 预解析:JS引擎会把JS里面所有的var还有function提升到当前作用域的最前面
  • 代码执行:按照代码书写的顺序从上往下执行

预解析分为变量预解析(变量提升)函数预解析(函数提升)

  • 变量提升:就是把所有的变量声明提升到当前作用域最前面,不提升赋值操作
  • 函数提升:就是把所有的函数声明提升到当前作用域的最前面,不调用函数

例1:

js
var num = 10;
fun()
function fun(){
    console.log(num); //输出undefined
    var num = 20;
}

例2:

js
f1();
console.log(c); //9
console.log(b); //9
console.log(a); //a is not defined (因为a是局部变量)
function f1(){
    var a = b = c = 9;
    //相当于var a = 9; b = 9; c = 9;b和c直接赋值,没有var声明,当全局变量看
    //集体声明:var a = 9, b = 9, c = 9;
    console.log(a); //9
    console.log(b); //9
    console.log(c); //9
}

new关键字的执行

  • 1、new构造函数可以在内存中创建一个空的对象
  • 2、this就会指向刚才创建的空对象
  • 3、执行构造函数里面的代码,给这个空对象添加属性和方法
  • 4、返回这个对象(所以构造函数里面不需要return)

闭包

  • 定义:能够读取其他函数内部变量的函数。(闭包=内层函数+外层函数的变量)(一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域)
  • 形成原理:作用域链,当前作用域可以访问上级作用域中的变量。
  • 好处:可以读取函数内部的变量,将变量始终保持在内存中,可以封装对象的私有属性和私有方法。能够让函数作用域中的变量在函数执行结束之后不被销毁,同时也能在函数外部可以访问函数内部的局部变量。
  • 坏处:耗费内存,使用不当会造成溢出。

原型链

所有的函数都有prototype属性(原型),所有的对象都有proto属性,在Javascript中,每个函数都有一个原型属性prototype指向自身的原型,而由这个函数创建的对象也有一个proto属性指向这个原型,而函数的原型是一个对象,所以这个对象也会有一个proto指向自己的原型,这样逐层深入直到Object对象的原型,这样就形成了原型链。

构造函数、实例、原型对象三者之间的关系:

An image

原型链:

An image

原型对象函数里面的this指向的是实例对象。

this指向

  • 全局作用域或者普通函数中this指向全局对象window(定时器里面的this指向window)
  • 方法调用中谁调用this指向谁
  • 构造函数中this指向构造函数的实例
  • 箭头函数不会创建自己的this,它的this指向上一层作用域的this指向
javascript
//函数的不同调用方式决定了this的指向不同
//1、普通函数this指向window
function fn(){
    console.log('hello');
}
// fn();  fn.call()
//2、对象的方法,this指向的是对象o
var o = {
    sayHi: function(){
        console.log('hello');
    }
}
o.sayHi();
//3、构造函数,this指向ldh这个实例对象,原型对象里的this指向的也是ldh这个实例对象
function Star(){};
Star.prototype.sing = function(){}
var ldh = new Star();
//4、绑定事件函数,this指向的是这个函数的调用者btn这个按钮对象
btn.onclick = function(){}; //点击按钮就可调用这个函数
//5、定时器函数,this指向的也是window
setInterval(function() {}, 1000);//定时器自动一秒钟调用一次
//6、立即执行函数,this也是指向window
(function(){
    console.log('hello');
})();
//立即执行函数是自动调用
//7、箭头函数的this指向上一层作用域的this指向
const fn = () => {
    console.log(this); //window
}
fn()
//7、
const obj = {
    uname: 'pink老师',
    sayHi: () => {
        console.log(this); //window
    }
}
obj.sayHi()
//7、
const obj = {
    uname: 'pink老师',
    sayHi: function(){
        let i = 10;
        const count = () => {
            console.log(this); //obj
        }
        count();
    }
}
obj.sayHi()

如何改变函数内this指向?

  • call方法

调用一个对象。简单理解为调用函数的方式,但是它可以改变函数的this指向。call的主要作用可以实现继承。

  • apply方法

调用一个函数。简单理解为调用函数的方式,但是它可以改变函数的this指向。

javascript
fun.apply(thisArg, [argsArray])
  • bind方法

不会调用函数,但是可以改变函数的this指向。

javascript
fun.bind(thisArg, arg1, arg2, ...)

thisArg:在fun函数运行时指定的this值。
arg1, arg2:传递的其他参数。
返回由指定的this值和初始化参数改造的原参数拷贝。
使用场景:如果有的函数我们不需要立即调用,但是又想改变这个函数内部的this指向,此时用bind(如:有一个按钮,当点击之后就禁用这个按钮,3秒钟之后开启这个按钮)。

箭头函数

  • 使用场景:箭头函数适用于本来就需要匿名函数的地方
  • 没有arguments动态参数,但有剩余参数...args
  • 不会创建自己的this,只会从自己的作用域链的上一层沿用this
  • call、apply、bind不会改变箭头函数的this指向
  • 箭头函数没有prototype
js
const obj = {
    uname: 'pink老师',
    sayHi: function(){
        let i = 10;
        const count = () => {
            console.log(this); //obj
        }
        count();
    }
}
obj.sayHi()

深拷贝和浅拷贝

浅拷贝

  • 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
    Object.assign

浅拷贝的实现:

  • 1.浅拷贝的局限性:只能拷贝一层对象。 如果存在对象的嵌套, 那么浅拷贝将无能为力
  • 2.对于基础数据类型做一个最基本的拷贝
  • 3.对引用类型开辟一个新的存储, 并拷贝一层对象属性

深拷贝

  • 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址
    JSON.parse(JSON.stringify(object))

深拷贝的思路:

  • 1.对于日期和正则的类型时, 进行处理new一个新的
  • 2.对a: { val: a } 这种循环引用时, 使用以weakMap进行巧妙处理
  • 3.使用Reflect.ownKeys返回一个由目标对象自身的属性键组成的数组
  • 4.对于剩下的拷贝类型为object和function但不是null进行递归操作
  • 5.对于除了上述的类型外直接进行"key"的赋值操作
  • 6.利用getOwnPropertyDescriptors返回指定对象所有自身属性(非继承属性)的描述对象
  • 7.将得到的属性利用Object.create进行继承原型链
  • 8.对于a: { val: a} 循环引用使用weakMap.set和get进行处理

手写深拷贝:

javascript
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {
  if (obj.constructor === Date) 
    return new Date(obj)       // 日期对象直接返回一个新的日期对象
  if (obj.constructor === RegExp)
    return new RegExp(obj)     //正则对象直接返回一个新的正则对象
  //如果循环引用了就用 weakMap 来解决
  if (hash.has(obj)) return hash.get(obj)
  let allDesc = Object.getOwnPropertyDescriptors(obj)
  //遍历传入参数所有键的特性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
  //继承原型链
  hash.set(obj, cloneObj)
  for (let key of Reflect.ownKeys(obj)) { 
  // 针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法
    cloneObj[key] = (isComplexDataType(obj[key]) && 
    typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
    //  typeof obj[key] !== 'function')
  }
  return cloneObj
}

var/let/const区别

  • let和const是块级作用域、不存在变量提升,var是函数级作用域、存在变量提升
  • var和let值可更改,const值不可更改

An image

注:变量和函数声明的提升:

在js中变量和函数的声明会提升到最顶部执行,函数的提升高于变量的提升,函数内部如果用 var 声明了相同名称的外部变量,函数将不再向上寻找。匿名函数不会提升。

JS运行机制

什么是单线程?和异步的关系?

  • 单线程 :只有一个线程,只能做一件事
  • 原因 : 避免 DOM 渲染的冲突,浏览器需要渲染 DOM,JS 可以修改 DOM 结构,JS 执行的时候,浏览器 DOM 渲染会暂停,两段 JS 也不能同时执行(都修改 DOM 就冲突了),webworker 支持多线程,但是不能访问 DOM
  • 解决方案 :异步

异步编程的实现方式

  • 回调函数(优点:容易理解;缺点:不利于维护、代码耦合高)
  • 事件监听(优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数;缺点:事件驱动型,流程不够清晰)
  • 发布/订阅(观察者模式):类似于事件监听,但是可以通过‘消息中心’,了解现在有多少发布者,多少订阅者
  • Promise对象(优点:可以利用then方法进行链式写法,可以书写错误时的回调函数;缺点:编写和理解相对较难)
  • Generator 函数 (优点:函数体内外的数据交换、错误处理机制;缺点:流程管理不方便 )
  • async函数(优点:内置执行器、更好的语义、更广的适用性、返回的是 Promise、结构清晰;缺点:错误处理机制 )

事件循环

是什么

首先,JavaScript是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环。

在JS中,所有任务都可以分为:

  • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
  • 异步任务:异步执行的任务,比如ajax网络请求,setTimeout定时函数等

同步任务与异步任务的运行流程图如下:

An image

从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就是事件循环。

宏任务与微任务

如果将任务划分为同步任务和异步任务并不是那么的准确,举个例子:

js
console.log(1)

setTimeout(()=>{
    console.log(2)
}, 0)

new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})

console.log(3)

如果按照上面流程图来分析代码,我们会得到下面的执行步骤:

  • console.log(1),同步任务,主线程中执行
  • setTimeout() ,异步任务,放到 Event Table,0 毫秒后console.log(2)回调推入 Event Queue 中
  • new Promise ,同步任务,主线程直接执行
  • .then ,异步任务,放到 Event Table
  • console.log(3),同步任务,主线程执行

所以按照分析,它的结果应该是 1 => 'new Promise' => 3 => 2 => 'then'

但是实际结果是:1=>'new Promise'=> 3 => 'then' => 2

出现分歧的原因在于异步任务执行顺序,事件队列其实是一个“先进先出”的数据结构,排在前面的事件会优先被主线程读取。

例子中setTimeout回调事件是先进入队列中的,按理说应该先于 .then 中的执行,但是结果却偏偏相反。

原因在于异步任务还可以细分为微任务与宏任务。

微任务

一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。

常见的微任务有:

  • Promise.then
  • MutaionObserver
  • Object.observe(已废弃;Proxy 对象替代)
  • process.nextTick(Node.js)

宏任务

宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合。

常见的宏任务有:

  • script (可以理解为外层同步代码)
  • setTimeout/setInterval
  • UI rendering/UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

此时事件循环、宏任务、微任务的关系如图所示:

An image

按照这个流程,它的执行机制是:

  • 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
  • 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完

回到上面的题目:

js
console.log(1)
setTimeout(()=>{
    console.log(2)
}, 0)
new Promise((resolve, reject)=>{
    console.log('new Promise')
    resolve()
}).then(()=>{
    console.log('then')
})
console.log(3)

流程如下:

js
// 遇到 console.log(1) ,直接打印 1
// 遇到定时器,属于新的宏任务,留着后面执行
// 遇到 new Promise,这个是直接执行的,打印 'new Promise'
// .then 属于微任务,放入微任务队列,后面再执行
// 遇到 console.log(3) 直接打印 3
// 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'
// 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2

async与await

async是异步的意思,await则可以理解为async wait。所以可以理解async就是用来声明一个异步方法,而await是用来等待异步方法执行。

async

async函数返回一个promise对象,下面两种方法是等效的:

js
function f() {
    return Promise.resolve('TEST');
}
// asyncF is equivalent to f!
async function asyncF() {
    return 'TEST';
}

await

正常情况下,await命令后面是一个Promise对象,返回该对象的结果。如果不是Promise对象,就直接返回对应的值。

js
async function f(){
    // 等同于
    // return 123
    return await 123
}
f().then(v => console.log(v)) // 123

不管await后面跟着的是什么,await都会阻塞后面的代码。

js
async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) // 阻塞
}

async function fn2 (){
    console.log('fn2')
}

fn1()
console.log(3)

上面的例子中,await会阻塞下面的代码(即加入微任务队列),先执行async外面的同步代码,同步代码执行完,再回到async函数中,再执行之前阻塞的代码。

所以上述输出结果为:1,fn2,3,2。

流程分析

例子:

js
async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('settimeout')
})
async1()
new Promise(function (resolve) {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')

分析过程:

1.执行整段代码,遇到 console.log('script start') 直接打印结果,输出 script start

2.遇到定时器了,它是宏任务,先放着不执行

3.遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到await怎么办?先执行 async2,打印 async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码

4.跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行

5.最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await下面的代码,打印 async1 end

6.继续执行下一个微任务,即执行 then 的回调,打印 promise2

7.上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout

所以最后的结果是:script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout

DOM文档对象模型

DOM树:

An image

  • 文档:一个页面就是一个文档,DOM中用document表示
  • 元素:页面中的所有标签都是元素,DOM中用element表示
  • 节点:网页中所有内容都是节点(标签、属性、文本、注释等),DOM中用node表示

DOM把以上内容都看作是对象

常见DOM操作

  • 创建节点
  • 添加节点
  • 删除节点
  • 复制节点

防抖节流

定义

  • 节流:n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
  • 防抖:n 秒后再执行该事件,若在 n 秒内被重复触发,则重新计时

实现

节流

  • 使用时间戳写法,事件会立即执行,停止触发后没有办法再次执行
js
function throttled1(fn, delay = 500){
    let oldtime = Date.now()
    return function(...args){
        let newtime = Date.now()
        if(newtime - oldtime >= delay){
            fn.apply(null, args)
            oldtime = Date.now()
        }
    }
}
  • 使用定时器写法,delay毫秒后第一次执行,第二次事件停止触发后依然会再一次执行
js
function throttled2(fn, delay = 500) {
    let timer = null
    return function (...args) {
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, args)
                timer = null
            }, delay);
        }
    }
}
  • 可以将时间戳写法的特性与定时器写法的特性相结合,实现一个更加精确的节流
js
function throttled(fn, delay) {
    let timer = null
    let starttime = Date.now()
    return function () {
        let curTime = Date.now() // 当前时间
        let remaining = delay - (curTime - starttime)  // 从上一次到现在,还剩下多少多余时间
        let context = this
        let args = arguments
        clearTimeout(timer)
        if (remaining <= 0) {
            fn.apply(context, args)
            starttime = Date.now()
        } else {
            timer = setTimeout(fn, remaining);
        }
    }
}

防抖

  • 简单版本
js
function debounce(func, wait) {
    let timeout;

    return function () {
        let context = this; // 保存this指向
        let args = arguments; // 拿到event对象

        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}
  • 防抖如果需要立即执行,可加入第三个参数用于判断,实现如下:
js
function debounce(func, wait, immediate) {

    let timeout;

    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout); // timeout 不为null
        if (immediate) {
            let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
            timeout = setTimeout(function () {
                timeout = null;
            }, wait)
            if (callNow) {
                func.apply(context, args)
            }
        }
        else {
            timeout = setTimeout(function () {
                func.apply(context, args)
            }, wait);
        }
    }
}

区别

相同点:

  • 都可以通过使用 setTimeout 实现
  • 目的都是降低回调执行频率,节省计算资源

不同点:

  • 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout和 setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
  • 函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次

例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次。

应用场景

防抖在连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染

节流在间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能

Promise

Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。一个Promise必然处于以下几种状态之一:

  • 待定 (pending): 初始状态,既没有被兑现,也没有被拒绝。
  • 已成功 (fulfilled): 意味着操作成功完成。
  • 已拒绝 (rejected): 意味着操作失败。

then方法下的promise:

fulfilled

  • 1.无return
js
const promise1 = Promise.resolve('promise1')
console.log('promise1', promise1); //fulfilled

const promise2 = promise1.then(() => {

})
console.log(promise2); //fulfilled
setTimeout(() => {console.log('promise2', promise2);}) //fulfilled
  • 2.有return,但不是return promise对象
  • 3.return fulfilled状态的promise对象
js
const promise1 = Promise.resolve('promise1')
console.log('promise1', promise1);

const promise2 = promise1.then(() => {
    return promise1
})
setTimeout(() => {console.log('promise2', promise2);}) //fulfilled

rejected

  • 1.return一个rejected的promise对象
js
const promise2 = promise1.then(() => {
    promise3 = new Promise((resolve, reject) => {
        reject('111') //rejected
    }) 
    return promise3
})
  • 2.throw error
js
const promise2 = promise1.then(() => {
    promise3 = new Promise((resolve, reject) => {
        throw new Error('err')
    }) 
    return promise3
})
  • 3.promise1就是rejected

pending

  • 1.resolve和reject都不执行
js
const promise4 = new Promise(() => {}) //pending
  • 2.return pending状态的promise