前端-js高级

339 阅读6分钟

一.对象、原型(链)

www.yuque.com/docs/share/…

1.JS数据类型

值类型和引用类型:

值类型(基本类型) :字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、Symbol

引用数据类型(对象类型) :对象(Object)、数组(Array)、函数(Function),还有两个特殊的对象:正则(RegExp)和日期(Date)

***注:***Symbol 是 ES6 引入了一种新的原始数据类型,表示独一无二的值。

2.对象

**1.所有的引用类型都可以被称为对象(所有引用类型都是广义的对象)。**至于Array、Function、Date、Regexp等都是对象的别名,就好像人这个类别中有鬼子还有棒子,但他们都是人一样。

2.值类型在需要的时候可以变成对象,但是是瞬间的。

3.在上面的所有对象中只有Object是血统最纯的,我称它为正经对象

4.对象不会凭空产生需要依赖函数产生 并且该函数需要以特殊的方式调用

5.每一个对象都必须new一个名称一样但是首字母是大写的函数产生(某一个对象都一定是由该对象制定的函数通过new 产生)

6.我们把能够产生某一类对象的函数叫构造函数(生产函数);

7.所有对象都有(隐式)原型(proto)

8.正经对象的原型的原型是null

9.对象的隐式原型 指向(构造函数)的原型 prototype属性

var o3 = {};//new object();

// new Function ——> function

// new Date ——> date

// new Array ——> array

// new Object ——> object

3.对象的特性

1.自由扩展性

const obj = {}
const arr = []
const fn = function () {}

obj.a = 1
arr.a = 1
fn.a = 1

console.log(obj.a) // 1
console.log(arr.a) // 1
console.log(fn.a) // 1

4.(隐式)原型

都有一个隐式原型 proto 属性,属性值是一个*正经的对象(P) *:

const obj = {};
const arr = [];
const fn = function() {}

console.log('obj.__proto__', obj.__proto__);
console.log('arr.__proto__', arr.__proto__);
console.log('fn.__proto__', fn.__proto__);

5.原型链(套娃)

blog.csdn.net/weixin_4269…

__proto__是所有对象的属性,对象的属性可以重新赋值。

var a = { a: 123 };
var b = { b: 234 };
console.log(a.a); // 123
console.log(b.b); // 234
console.log(a.b); // undefined
a.__proto__ = b;
console.log(a.b); // 234
console.log(a.toString()); 

6.构造器(构造函数)

可以通过new关键字操作产生一个对象的函数我们称为构造函数(constructor)。

构造函数首字母大写

什么构造函数就能new出什么对象比如:

new 数组构造函数() => 数组对象

new 函数构造函数() => 函数对象

7.显式原型

每一个构造函数都具有一个prototype属性

对象的构造函数的显式原型指向该对象的隐式原型如:Array.prototype === [].proto

构造函数的 显式原型的constructor属性指向构造函数本身

最后一个 null,设计上是为了避免死循环而设置的, Object.prototype 的隐式原型指向 null。

8.instanceof

instanceof 运算符用于测试构造函数的 prototype 属性是否出现在对象原型链中的任何位置。

instanceof 
// 是用来判断左侧对象是否是右侧构造函数的实例化对象,

// 或则说左侧对象能否通过其隐式原型 

// [[proto]]在原型链上一层层向上查找到右侧函数的原型对象,

// 即函数原型对象出现在实例对象的原型链上就返回 true。

// 通俗的理解:右侧是不是左侧的爸爸、爷爷、祖宗,只要左侧对象继承自右侧函数就为true
// instanceof实现原理
// 参数:object 实例化对象 construcotr构造函数
function $instanceof(object , construcotr){
    var prototype = construcotr.prototype;//声明变量prototype存放构造函数的显示原型
    while(true){
        object = object.__proto__;//形参object存放实例化对象的隐式原型,若没找到,object=object的隐式原型的隐式原型,以此类推
        if(object === null){ //找到原型链最后是null返回false
            return false;
        }
        if(object === prototype){ //隐式原型 === 构造函数的显示原型
            return true;
        }
    }
}
console.log($instanceof([],Object));//true
console.log($instanceof([],Date));//false

9.判断一个对象是某个构造函数产生的

//判断一个对象是某个构造函数产生的
function isParent(object,construcotr){
    // 构造器的显示原型 === 实例化对象的隐式原型   返回true
    return construcotr.prototype === object.__proto__;
}

console.log(isParent([],Array));//true
console.log(isParent([],Object));//false

10.hasOwnProperty

//hasOwnProperty表示是否有自己的属性。这个方法会查找一个对象是否有某个属性,但是不会去查找它的原型链。

// for in 循环
//循环obj的自有属性
var obj = {a:1,b:2,c:3};
obj.__proto__ = {d:1234};//给obj的隐式原型进行赋值
for(var i in obj){
   if(obj.hasOwnProperty(i)){ //判断i是obj的自有属性
        console.log(i,obj[i]);//只打印obj自己的,原型链上不属于自己的
   }
}

11.Object.defineProperty()

var obj = {a:1,b:2,c:3};
Object.defineProperty(obj,'b',{enumerable:false});//将属性设为不可枚举
for(var i in obj){
    console.log(i,obj[i]);
}

//Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
//注意:应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。

//语法:Object.defineProperty(obj, prop, descriptor)
//参数
// obj
// 要定义属性的对象。
// prop
// 要定义或修改的属性的名称或 Symbol 。
// descriptor
// 要定义或修改的属性描述符。
    // 属性值1:value
    // 设置属性默认值
    // 属性值2:writable
    // 设置属性是否能够修改
    // 属性值3:enumerable
    // 设置属性是否可以枚举,即是否允许遍历
    // 属性值4:configurable
    // 设置属性是否可以删除或编辑
    // 属性值5:get
    // 获取属性的值
    // 属性值6:set
    // 设置属性的值

//enumerable 定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。

二、作用域/链

www.yuque.com/docs/share/…

1.作用域原理

隔离变量 使我的变量不受影响

2.立即执行函数

立即执行函数只有一个作用:*创建一个独立的作用域。*这个作用域里面的变量,外面访问不到(即避免了「变量污染」)

(function () {
    var a = 3;
})();//立即执行函数

3.执行上下文

执行上下文就是在JS代码正式执行之前去做一些准备

全局执行

//准备工作之一是:扫描整体代码 把全局的变量声明提前 并给定默认值为undefined

//准备工作之二是:对this进行赋值

//准备工作之三是:对函数声明整个提前,函数表达式同准备一

函数执行

//函数执行上下文环境 (函数执行前的准备工作)

//1.变量声明赋值为undefined

//2.确定this的取值

//3.确定函数声明的取值

//4.确定arguments的取值

//5.确定自由变量的取值

//作用域在定义的时候就已经确定了

4.自由变量

假如在全局中定义了变量a,在函数中使用了这个a,这个a就是自由变量,可以这样理解,凡是跨了自己的作用域的变量都叫自由变量。

函数在定义的时候就确定了函数中自由变量的取值永远不会变

5.作用域

作用域代表了一个变量的合法范围,一个变量的作用域是程序源代码中定义的这个变量的区域

全局作用域

不在任何函数内声明的变量(函数内省略var的也算全局)称作全局变量 就是在最外层定义的变量就被称为全局变量,全局都可以使用,所以是全局作用域

局部作用域

在函数内声明的变量具有函数作用域,属于局部变量,就是在函数内部定义的变量, 只在函数内部有用,所以是局部作用域

6.闭包

一个作用域/函数访问另一个函数的内部变量(局部变量)

作用是延伸了变量的作用范围

三、构造函数、this、改变this指向

www.yuque.com/docs/share/…

1.构造函数

构造函数的特征

// 首字母大写的函数(约定俗成的规范)

// 具有显示原型

// 调用

// 需要配合new关键字

// 结果

// 调用后返回一个对象

函数被new之后,帮我们做了什么??(new关键字)

// 1.新建一个对象

// 2.this指向这个对象

// 3.给this(对象)赋值

// 4.自动返回值this/对象

// 5.给this添加一个隐式原型__proto__ 等于函数的显示原型赋值prototype

2.this指向

不管是什么函数 只要不执行 就不谈this指向

构造函数 --> 实例化 --> 实例

构造函数的返回值我们称为实例

// 用new修饰的构造函数的执行结果 我们称之为实例化

// 1.构造函数被new修饰执行的时候 this指向其返回值

3.改变this指向

//改变this指向 call apply bind
        function foo() {
            console.log(this);
        };

        console.dir(Function.prototype)
        // foo.call(); //改变this指向到第一个参数  并立即执行
        // foo.apply(); //改变this指向到第一个参数  并立即执行
        // foo.bind(); //改变this指向到第一个参数  返回新函数 需要手动执行
        foo();//window
        var o = {
            name: "o",

        }
        o.f = foo;
        o.f();// this --> o
        foo.call(o); //this --> o
        foo.apply(o);//this --> o
        foo.call(undefined); //this --> undefined
        foo.call(123);//123
        o.f.apply([123]) //this-->[123]

        function sum(x, y) {
            this.sum1 = x + y;
        }
        sum(1, 2);// 直接执行相当于声明了一个全局变量 var sum = 3;
        sum.call(o, 1, 2);//sum执行时this --> o 参数列表[1,2]
        sum.apply(o, [1, 2]);//sum执行时this --> o 参数列表[1,2]
        var fn = sum.bind(o, 1, 2);//sum执行时this --> o 参数列表[1,2] 不会自动执行  需要手动调用 
        fn();
        console.log(o);

四、递归、深浅拷贝

www.yuque.com/docs/share/…

1、递归

①概念:函数自己调用自己本身,或者在自己函数调用的下级函数中调用自己。

②递归的步骤

假设递归函数已经写好

寻找递推关系

将递推关系的结构转换为递归体

将临界条件加入到递归体中

2、深拷贝

对于值类型来说,赋值即拷贝(深拷贝)

2、检测数据的类型

只有正经对象{}的原型上的toString方法能正确且准确的返回调用者的类型

function getType(data){
        //用call或者apply改变this的指向
    return Object.prototype.toString.call(data).slice(8,-1).toLowerCase();
}

返回值:值类型'number','string','boolean','undefined','null','symbol'
          引用类型:'array','object','date','function','regexp'

3、深拷贝(考虑值类型、数组、对象)

function deepClone(target) {
    if (typeof target === 'object') {
        let cloneTarget = Array.isArray(target) ? [] : {};
        for (const key in target) {
            cloneTarget[key] = deepClone(target[key]);
        }
        return cloneTarget;
    } else {
        return target;
    }
};

4、深拷贝(考虑值类型、数组、对象、正则、函数、日期)

需要先写getType这个函数,准确返回调用者的类型

function deepClone(target){
        //用instanceof来区分值类型和引用类型
    if(target instanceof Object){
        if(getType(target) === 'date'){
                return new Date(target.getTime());
        }
        if(getType(target) === 'regexp'){
                return new RegExp(target.source);
        }
        if(getType(target) === 'function'){
                return new Function(target.toString());
        }
        
        var res = getType(target) === 'object' ? {} : [];
        for(var i in target){
                res[i] = deepClone(target[i]);
        }
        return res;
    }else{
            return target;
    }
}

五、数组的方法

1、forEach

// forEach的参数只有一个是函数对象(带3个形参)
// 作用是自动循环执行传给forEach的参数(函数) 并传入3个实参 分别是:当前循环的值 当前循环的索引 当前数组
// 返回值是undefined

//forEach的实现原理
Array.prototype.each = function(fn){
    if(!this.length) return;
    for(var i=0;i<this.length;i++){
            fn(this[i],i,this);
    }
}

2、map

//forEach的功能map都有 但是还有过人之处
// 返回值:得到一个和原数组长度相同的新数组

//map的实现原理
Array.prototype.$map = function(callback){
        var result = [];
        for(var i=0;i<this.length;i++){
                result.push(callback(this[i],i,this));
        }
        return result;
}

3、every

//如果该数组的所有值都满足你的条件 返回true 否则false
var arr = [1,3,5,7,-1];
var res = arr.every(function(value){
    return value % 2 != 0;//条件--false
});

//every的实现原理
Array.prototype.$every = function(callback){
        var flag = true;
        for(var i=0;i<this.length;i++){
                if(!callback(this[i],i,this)){
                        flag = false;
                        break;
                } 
        }
        return flag;
}

4、some

//该数组只要有一个满足你的条件 返回true 否则false
var arr = [1,3,5,7,-1];
var res = arr.some(function(value){
    return value % 2 != 0;//条件--true
});

//some的实现原理
Array.prototype.$some = function (callback){
        var flag = false;
        for(var i=0;i<this.length;i++){
                if(callback(this[i],i,this)){
                        flag = true;
                        break;
                }
        }
        return flag;
}

5、filter

// 返回满足条件元素组成的新数组

//filter的实现原理
Array.prototype.$filter = function (callback){
        var res = [];
        for(var i=0;i<this.length;i++){
                if(callback(this[i],i,this)){
                        res.push(this[i]);
                }
        }
        return res;
}

6、reduce和reduceRight

var arr = [1,2,3,4];
var res = arr.reduce(function (prev,value,index,array){
     return prev + value;
},0);

// reduce 的第二个参数是在函数执行前prev的初始值
// 在每一次循环后 都会将参数函数的返回值赋值给prev
// 当循环结束 prev会作为reduce的返回值
// reduce是从左往右加,reduceRight是从右往左加

console.log(res);

//reduce的实现原理
Array.prototype.$reduce = function (callback,initValue){
   for(var i = 0;i<this.length;i++){
      initValue = callback(initValue,this[i],i,this);
   }
   return initValue;
}

7、find和findIndex

//find作用,对数组进行循环,找出第一个满足条件的value。没有找到返回undefined

var arr = [11,22,33,44,55,66,77,88];
var res = arr.find(function (value,index,array){
        return value % 2 == 0;
});

//findIndex作用,对数组进行循环,找出第一个满足条件的value的索引。没有找到返回-1
var res1 = arr.findIndex(function (value,index,array){
        return value % 2 == 0;
});
console.log(res,res1);

//find实现原理
Array.prototype.$find = function (callback){
   for(var i = 0;i<this.length;i++){
      if(callback(this[i],i,this)) return this[i];
   }
   return undefined;
}

//findIndex实现原理
Array.prototype.$findIndex = function (callback){
   for(var i = 0;i<this.length;i++){
      if(callback(this[i],i,this)) return i;
   }
   return -1;
}

8、includes

//用来判断一个数组是否包含一个指定的值,如果是返回 true,否则false。

//includes实现原理
Array.prototype.$includes = function (target){
   for(var i = 0;i<this.length;i++){
      if(this[i] == target){
          return true;
      }
   }
   return false;
}

9、flat

按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

// 将多维数组变成一维数组
// console.log(arr.flat(Infinity));

六、stack栈

www.yuque.com/docs/share/…

密码:cn1d

1.栈

栈与数组的区别是 栈只能在一端操作

栈是一种遵从后进先出(LIFO)原则的有序集合。新添加或待删除的元素都保存在栈的同 一端,称作栈顶,另一端就叫栈底

2.创建一个栈并给栈原型上添加方法

function Stack() {
        for (var i = 0; i < arguments.length; i++)  this[i] = arguments[i];
        this._size = i;
    }

 push(element(s)):添加一个(或几个)新元素到栈顶。

 push: function (element) {
            this[this._size] = element;
            return ++this._size;
        },

 pop():移除栈顶的元素,同时返回被移除的元素。

pop: function () {
            var popItem = this[this._size - 1];
            delete this[this._size - 1];
            this._size--;
            return popItem;
        },

 peek():返回栈顶的元素,不对栈做任何修改(该方法不会移除栈顶的元素,仅仅返回它)。

 peek: function () {
            return this[this._size - 1];
        },

 isEmpty():如果栈里没有任何元素就返回 true,否则返回 false。

isEmpty: function () {
            return this._size === 0;
        },

 clear():移除栈里的所有元素。

 clear: function () {
            for (var i = 0; i < this.length; i++) {
                delete this[i];
            }
            this._size = 0;
        },

 size():返回栈里的元素个数。该方法和数组的 length 属性很类似。

size: function () {
            return this._size;
        },

3、转换进制

 // 函数功能:
    // num:    要转换的值  返回字符串
    // base:   几进制
    function binary1(num, base) {
        var res = '';
        var arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
        var stack = new Stack();
        while (num > 0) {
            stack.push(num % base);
            num = parseInt(num / base);
        }
        while (!stack.isEmpty()) {
            res += arr[stack.pop()];
        }
        return res;
    }
    console.log(binary1(100345, 16));

4、有效括号、回文数

//有效的括号
    //[](){}  [{()}]  [{()})
    function isValid(s) {
        if (s.length % 2 !== 0) return false;
        var stack = new Stack();
        var kv = { "(": ")", "[": "]", "{": "}" };
        for (var i = 0; i < s.length; i++) {
            if (stack.isEmpty()) {
                stack.push(s[i]);
            } else {
                if (kv[stack.peek()] === s[i]) {
                    stack.pop();

                } else {
                    stack.push(s[i]);
                }
            }
        }
        return stack.isEmpty()
    }
    var s = "{[])";
    console.log(isValid(s));
// 回文数
    //    1 11  111  121 12321 123321 
    // 长度为1
    // 长度为偶数  对称相等
    //长度为奇数且大于1  去掉中位数  左右对称

    function isPalindrome(n) {
        n = n.toString();
        if (n.length === 1) return true;
        var stack = new Stack();
        var middleIndex = n.length % 2 === 0 ? -1 : Math.floor(n.length / 2);

        for (var i = 0; i < n.length; i++) {
            if (i !== middleIndex) {
                if (stack.isEmpty()) {
                    stack.push(n[i]);
                } else {
                    if (stack.peek() === n[i]) {
                        stack.pop();
                    } else {
                        stack.push(n[i]);
                    }
                }
            }
        }
        return stack.isEmpty();


    }
    console.log(isPalindrome("123321"));
    console.log(isPalindrome("123454421"));

七、ajax

1、发送请求

 //type  String
    //url  String
    //data  Object
    //success  Function
    //fail  Function
  

    function request(config) {
        function getTypeOf(data) {
            //给对象的构造函数的原型上添加一个能精准返回调用者的类型的方法
            return Object.prototype.toString.call(data).slice(8, -1).toLowerCase();
        }
        function formateParams(data) {
            if (!data) return "";//如果传进来data对象不存在就返回空字符串
            if (getTypeOf(data) !== "object") {//判断是不是对象
                throw new Error("请求参数必须是对象");//如果不是对象就报错并抛出
            }
            var queryString = "?";//声明一个字符串
            for (var key in data) {//遍历data对象
                queryString += key + "=" + data[key] + "&"; //把对象里的键值对拼接起来
            }
            return queryString.slice(0, -1);//截取这个字符串,去掉最后的一个&符
        }
        if (!config.url) return;//如果地址为空就不发送请求
        var xhr = new XMLHttpRequest();//创建可以对服务器发送请求的对象
        //调用open的方法传的参数没有类型就默认get,需要请求数据的地址+处理后条件数据
        xhr.open(config.type || "get", config.url + formateParams(config.data));
        //调用函数的onreadystatechange的方法(请求状态发生变化的方法,如果readyState发生变化就会触发)
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {//请求状态码为成功 请求已被成功接收 理解  接受
                if (xhr.status === 200) {//服务器处理数据状态码为完成
                    var json = JSON.parse(xhr.response);//把服务器返回的字符串转换为对象
                    if (getTypeOf(config.success) === "function") {//判断请求完成返回的是否是函数
                        config.success(json);//把服务器返回被转换为对象的值传给success方法的实参
                    }
                } else {//失败
                    if (getTypeOf(config.fail) === "function") {//判断请求失败返回的是否是函数
                        config.fail(xhr.status);//把失败的服务器状态码返回给fail控制台打印
                    }
                }
            }
        };
        xhr.send()//发送请求
    }

2、调用函数传参

 //  https://music.qier222.com/api/album?id=147687873&realIP=211.161.244.70

    // 调用函数传一个对象
    request(
        {
            url: "https://music.qier222.com/api/album",//请求数据的地址
            data: {
                id: "147687873",
                // id: "147688331",
                realIP: "211.161.244.70",
            },
            // 服务器返回状态码为成功的函数
            success: function (res) {
                //渲染歌曲列表
                console.log(res);
                renderSongs1(res.album, res.songs)
                renderSongs(res.songs);
            },
            // 服务器返回状态码为失败的函数
            fail: function (err) {
                console.log(err);//打印失败返回的服务器返回的状态码
            },
        });

3、渲染数据

  function renderSongs(songs) {
        var songsElement = document.querySelector(".songs");//获取节点
        // 在节点里写入内容
        songsElement.innerHTML = songs
            //返回的songs用数组map()方法返回一个我们需要的数组数据,再用join变成一个字符串 并写入节点中
            .map(function (value, index) {
                return `
                <div class="songs-item">
                    <div class="songs-item_name">${index + 1}.${value.name}</div>
                    <div class="songs-item_duration"> ${timeLength(value.dt)}</div>
                </div>
                `;
            })
            .join("");
        console.log(songs)
    };

4、获取数据

a标签

<a href="./datail.html?id=123"> 跳转到详情页</a>
    window.onload = function () {
        var params = {};
        console.log(
            location.search
                .slice(1)
                .split("&")
                .map(function (value) {
                    return value.split("=");
                })
                .forEach(function (value) {
                    params[value[0]] = value[1];
                })
        );
        console.log(params);
    };

八、Promise对象

www.yuque.com/docs/share/…

1、Promise

Promise 解决异步回掉嵌套问题

2、Promise承诺

//Promise 承诺

//一个承诺的兑现是需要时间

//一个承诺是否兑现是有一个结果

/**

找xxx借钱 (一个小目标) 时长一个月

形成一个承诺:一个月之后还

情况1:等待状态(pending) => 兑现状态(resolved)

情况2:等待状态(pending) => 不能兑现状态(rejected)

承诺的结果:

如果兑现:xxx收到一个亿

如果未兑现:xxx会收到一个原因

**/

//Promise 处理异步操作(耗时操作)

//常见的异步操作:setTimeout setInterval xhr

//实例化Promise

//Promise 接受一个函数作为参数,而且该函数有两个形参 分别是resolve,reject

//原型方法:then catch finally

//实例属性:[[PromiseState]] [[PromiseResult]]

//通过函数表达承诺的具体内容
    // resolve:兑现承诺
    // reject:兑现不了承诺
    var p2 = new Promise(function (resolve, reject) {//实例化一个
        setTimeout(function () {
            if (Math.random() > 0.5) {
                resolve("10元,拿走不谢!");
            } else {
                reject("10块太多,目前手上没有那么多,分12期免息还款");
            }
        }, 3000);
    });
    //Promise实例的then方法的第一个参数函数  一定是在承诺被兑现的时候执行
    // p2.then(function (res) {
    //     console.log(res);
    // }, function (err) {
    //     console.log(err);
    // })

九、Event Loop

www.yuque.com/docs/share/…

1、任务队列

//任务队列是存储js任务的(宏任务和微任务)

//宏任务 (task)--- 主任务:整体的script脚本、setTimeout、setInterval 事件

//微任务(micro task):Promise.prototype.then/catch/finally

console.log("开始");
Promise.resolve().then(function () { //w1
    console.log(1);

});
setTimeout(function () { //h1
    console.log(2)
    Promise.resolve().then(function () { //w3
        console.log(11);
    });
}, 0);
console.log(3)

new Promise(function (resolve, reject) {
    reject(6);
    console.log(4);
})
    .then(function (res) {
        console.log(res);
    })
    .catch(function (err) { //w2
        console.log(err);
    });
console.log(3);

// "开始"-3-4-3-1-6-2-11
//首先代码执行
//当一个宏任务结束时会去检查由该任务产生的任务队列,会先去清空微任务队列

1.整体的js代码就是一个匿名函数构成的宏任务

2.宏任务在执行的过程中可能会产生若干个子宏任务微任务

3.只有当宏任务执行结束且执行栈为空的时候才会去关注异步任务(宏任务微任务)并且优先清空微任务队列

4.当微任务队列清空之后再开始下一次宏任务

// 事件是宏任务
document.body.onclick = function () {
    console.log(1111);
}

rAF

//系统自己触发 根据刷效率 60HXZ 每16.7ms执行一次
requestAnimationFrame(function () {
    console.log(123);
})
// requestAnimationFrame 宏任务

每一轮事件循环会经历:

宏任务 宏任务衍生的所有微任务 rAF队列 UI更新

//保证每一次页面刷新都会执行一次 
// 执行这一次的时刻在当前这一轮event loop循环中 微任务清空结束之后 UI更新之前
requestAnimationFrame(function () {
    console.log(2);
});
setTimeout(function () {
    console.log(1);
}, 100);
requestAnimationFrame(function () {
    console.log(3);
});
console.log(0);
Promise.reject(4).catch(function (err) {
    console.log(err);
})
//每一轮事件循环会经历:宏任务 宏任务衍生的所有微任务 rAF队列 UI更新
//0- 4 - 2 - 3 - UI更新 - 1
var el = document.querySelector("div");
var width = 0;
function animate() {
    requestAnimationFrame(function () {
        el.style.width = `${++width}px`;
        animate();
    })
}
animate();

// 约等于
// setInterval(function () {
//     el.style.width = `${++width}px`;
// }, 16.7);

每一次循环rAF都会创建一个任务 下一次循环执行故而不会死循环