美文网首页
[深入20] 手写函数

[深入20] 手写函数

作者: woow_wu7 | 来源:发表于2021-09-22 08:41 被阅读0次

    导航

    [深入01] 执行上下文
    [深入02] 原型链
    [深入03] 继承
    [深入04] 事件循环
    [深入05] 柯里化 偏函数 函数记忆
    [深入06] 隐式转换 和 运算符
    [深入07] 浏览器缓存机制(http缓存机制)
    [深入08] 前端安全
    [深入09] 深浅拷贝
    [深入10] Debounce Throttle
    [深入11] 前端路由
    [深入12] 前端模块化
    [深入13] 观察者模式 发布订阅模式 双向数据绑定
    [深入14] canvas
    [深入15] webSocket
    [深入16] webpack
    [深入17] http 和 https
    [深入18] CSS-interview
    [深入19] 手写Promise
    [深入20] 手写函数

    [react] Hooks

    [部署01] Nginx
    [部署02] Docker 部署vue项目
    [部署03] gitlab-CI

    [源码-webpack01-前置知识] AST抽象语法树
    [源码-webpack02-前置知识] Tapable
    [源码-webpack03] 手写webpack - compiler简单编译流程
    [源码] Redux React-Redux01
    [源码] axios
    [源码] vuex
    [源码-vue01] data响应式 和 初始化渲染
    [源码-vue02] computed 响应式 - 初始化,访问,更新过程

    前置知识

    包装对象

    • <font color=red>包装对象:Number,String,Boolean三个对象可以把原始类型的值变成对象 -> new 调用</font>
    • number,string,boolean三个原始类型的值,在一定的条件下也会自动转为对象,也就是原始类型的包装对象
    • 包装对象的目的
      • 使得js中的对象涵盖所有的值
      • 使得 ( 原始类型 ) 的值,可以方便的 ( 调用某些方法 )
        • 比如 toString valueOf
    • <font color=red>总结</font> Nubmer,String,Boolean
      • 作为构造函数:通过new命令调用,可以将 ( 原始类型 ) 的值转为 ( 对象 )
      • 作为普通函数:可以将 ( 任何类型 ) 的值,转换成 ( 原始类型 ) 的值

    构造函数

    • 特点:
      • 构造函数首字母通常大写
      • 使用 new 命令调用,new命令调用时,可以在函数名后面加小括号,也可以不加
      • 构造函数内部的this指向实例对象
      • 使用构造函数,属性和方法都生成在实例上的,彼此之间不共享
      • <font color=red>new 命令总会返回一个对象</font>,要么是实例对象,要么是return后指定的对象
        • 如果构造函数内 ( return ) 返回的是一个 ( 对象 ),new命令就会返回回这个 ( 对象 )
        • 如果构造函数内 ( return ) 返回的是 ( 原始类型的值 ),new命令就会返回 ( this对象 )
        • ( 普通函数 ) 使用 ( new )命令,会返回一个 ( 空对象 )
    • new命令的原理
      • 创建一个空对象,即是需要返回的那个实例对象
      • 将空对象的原型,指向构造函数的prototype属性
      • 将构造函数内部的this绑定到这个空对象上
      • 执行构造函数

    一些单词

    immediate: 立即的
    shuffle: 洗牌
    
    flatten:扁平的
    infinity:无穷
    
    recursive:递归
    

    call 模拟实现

    • call方法的作用

      • 绑定函数的 this 指向
      • 执行该函数 ( 在指定的作用域中执行该函数 )
    • call的参数

      • 应该是一个 ( <font color=red>对象</font> )
      • 如果传入 ( <font color=red>null,undefined,空</font> ),则默认传入 ( <font color=red>全局对象</font> ) window/global
      • 如果参数是 ( <font color=red>原始值</font> ) 那么这个原始值会自动转成对应的 ( <font color=red>包装对象</font> ),然后传入call方法
      • 可以接收多个参数
        • 第一个参数是 this 要绑定的对象
        • 后面的参数是函数调用时需要的参数
    • call方法的作用应用 ( 重要 )

      • call方法的一个应用就是 - 调用对象的原生方法
      • 当一个对象调用继承的属性时,如果该对象自身也有一个同名的属性时,将会发生覆盖
      • call方法可以把继承的方法的原始定义放在该对象上执行,这样无论该对象是否有同名的属性,都不会受影响
    [1]
    // call方法的应用
    // - call方法的一个应用就是 - 调用对象的原生方法
    -------
    
    var obj = {};
    obj.hasOwnProperty('toString') 
    // false,注意toString是继承的方法,空对象自身并没有该方法
    // hasOwnProperty 表示是否含有自身属性
    
    
    // 覆盖掉继承的 hasOwnProperty 方法
    // 通过在obj自己定义一个方法,这样就会覆盖原型链上的同名方法,并且这样就是自身属性
    obj.hasOwnProperty = function () {
      return true;
    };
    
    obj.hasOwnProperty('toString') 
    // true,调用时先找自身是否有该方法,而本意是调用原型链上的hasOwnProperty方法
    // 这样就造成了结果偏差,可以用call方法解决
    // call方法的一个主要应用 - 调用对象的原生方法
    
    Object.prototype.hasOwnProperty.call(obj, 'toString') // false
    
    
    上面代码中,hasOwnProperty是obj对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。
        - call方法可以解决这个问题 - 调用对象的原生方法
        - 它将hasOwnProperty方法的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果。
    
    • call方法的模拟实现 - es5
    ### call方法的模拟实现 - es5
    
    注意点:
    1. fn.call() 的参数
        - 当为空,null,undefined时,表示传入全局对象window或者global
        - 当为原始类型的值时,会被转为包装对象再传入fn
        - 第一个参数是fn中this指向的对象,其他参数将作为调用fn时的参数
    2. fn可以有返回值
    3. 实现思路
        - 在对象上 (添加) 函数 (方法)
        - 在该对象上(执行)该函数,此时this的指向就是该对象(运行时确定this指向)
            - 因为fn.call()是可以接收不定个数的参数的,怎么办?
            - 可以用arguments对象获取所有参数,这里需要去除第一个参数即this绑定的对象,后面的才是传入fn函数的参数
            - 记得注意下 fn 也可以有返回值
        - (删除)该函数
    4. eval(string)
        - 将字符串当作语句来执行
        - 注意:参数必须是字符串
        
    
    
    代码:
    const obj = {
      name: 'woow_wu7',
      age: 18
    }
    function fn(address, sex) {
      return this.name + this.age + sex + address 
    }
    
    Function.prototype._call = function(obj) {
      const context = obj || window
      // 相当于 context = context ? context : window;
      // 相当于 const context = obj ? obj : window;
      // fn.call() 当参数为空,null,undefined时,默认是传入window对象
      // 空,null,unfined的布尔值都是false
    
      const params = []
      // 用于收集arguments中出去第一个参数外,剩下的每一个参数
    
      for(let i = 1; i < arguments.length; i++) {
        params.push('arguments[' + i + ']')
      }
      // 注意:循环是从 1 开始的,因为arguments[0] 是fn中this绑定的对象,这里是收集传入fn的参数
      // arguments是类似数组的对象,具有length属性
      // params相当于 ['arguments[1]', 'argument[2]']
    
    
    
      context.fn = this
      // --- 第一步:在对象上添加一个函数属性fn(对象中应该叫做方法)
      // 函数的值是this,即 fn 函数
      // this这里指的是 fn 函数,因为this就是函数调用时所在的对象,fn._call()是fn在调用,所以this指向fn
    
    
    
      const res = eval('context.fn(' + params + ')')
      // fn函数可能有返回值
      // + 号是存在(重载)的,即可以是(相加)或者(相连)
      // 当 + 运算子存在对象时,会先将对象转成原始类型的值,即先执行 valueOf -> 对象本身 -> toStirng -> 出object外,都是返回该类型对象的字符串形式
      // params数组会被转成 'arguments[1],arguments[2]'这样的形式
      // 例如:[1,'2',3,'4'].toString() -> "1,2,3,4"->  数组返回数组的字符串形式
      // 最终是这样的: const res = eval('context.fn(arguments[1],arguments[2])')
    
      Reflect.deleteProperty(context, 'fn')
      // delete context.fn
      // 删掉对象上的函数
      // 注意:delete context.fn 这样的旧的写法会逐渐被抛弃
    
      return res
      // fn可能有返回值,需要返回结果
    }
    
    const res = fn._call(obj, 'chongqing', 'man')
    console.log(res, 'res')
    
    • call方法的模拟实现 - es6
    ### call方法的模拟实现 - es6
    
    const obj = {
      name: 'woow_wu7',
      age: 18
    }
    function fn(address, sex) {
      return this.name + this.age + sex + address 
    }
    
    Function.prototype._call = function(context) {
      context = context ? context : window;
      context.fn = this;
      const res = context.fn(...[...arguments].slice(1));
      
      Reflect.deleteProperty(context, 'fn')
      //delete context.fn;
      
      return res;
    }
    const res = fn._call(obj, 'chongqing', 'man')
    console.log(res, 'res')
    
    

    apply模拟实现

    const obj = {
      name: 'wang',
    };
    function fn(name) {
      return {
        name: name || this.name
      }
    }
    Function.prototype._apply = function (context, arr) {
      context = context ? context : window;
      context.fn = this;
      let res = null;
      if (!arr) { //_apply第二个参数不存在,就不给fn传参,直接执行fn
        res = context.fn()
      } else {
        arr instanceof Array
        ?
        res = context.fn(...arr)
        :
        console.log('第二个参数只能是数组')
      }
      delete context.fn;
      return res;
    }
    const result = fn._apply(obj, ['woow-wu']);
    console.log(result)
    

    bind模拟实现

    • bind函数的作用
      • 返回一个新的函数
      • 绑定this的指向
    • bind函数的参数
      • 第一个参数:需要绑定的对象
      • 后面的参数:作为函数的参数
    • 注意点:
      • 当第一个参数是null,undefined时,相当于讲this绑定到全局对象上( window|global )
      • 除去第一个参数外,剩余的参数作为 ( 需要绑定this的函数的全部或者部分参数 )
        • bind时可以只传部分参数
        • 剩余的参数在返回的新函数被调用时传入
      • <font color=red>返回的新函数,可以使用new命令调用,即返回的新函数可以作为 ( 构造函数 )</font>,这种情况下
        • bind时绑定的this失效,<font color=red>因为在构造函数中,this指向的是实例对象</font>
        • 但是传入的参数有效
    • 代码模拟实现
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <script>
            const obj = {
                name: 'woow_wu7',
                age: 20
            }
            function fn(sex, address) {
                this.sex = sex
                this.address = address
                return this.name + this.age + sex + address
            }
            fn.prototype.go = function() {
                console.log('hangzhou')
                return 'hangzhou'
            }
            // 当_bind()返回的新函数通过new命令调用时,生成的实例可以继承fn.prototype上的方法
    
            Function.prototype._bind = function(context) {
                context = context ? context : widnow
                // 传入的第一个参数
                // 如果是null或者undefined时,相当于传入全局对象 ( window|global )
    
                const params = Array.prototype.slice.call(arguments, 1)
                // params:是外层函数接受的要传入被绑定函数的参数,除去第一个参数即除去绑定的对象
                // arguments:实参列表
    
                const self = this
                // 固定外层函数的this
    
                const Ftemp = function(){}
                // 寄生式继承
                // 构造函数首字母通常大写
    
                const fbind = function() {
                    const bindParams = Array.prototype.slice.call(arguments)
                    // fbind函数的实参
    
                    const totalParams = params.concat(bindParams)
                    // 所有要传入被绑定函数的参数数组
    
                    return self.apply(this instanceof self ? this : context, totalParams)
                    // self:是外层的this,调用时确定指向,即fn函数
                    // this:fbind函数中的this,如果是构造函数this就指向实例,如果不是就执行需要绑定的对象
                    
                    // this instanceof self ? this : context
                    // 因为:下面将 fbind.prototype => new Ftemp => this.prototype
                    // 所以:如果是new命令在调用fbind的话,判断是true,绑定到实例this上,否则绑定到传入的对象上
    
                    // 可能有返回值,需要return
                }
    
                Ftemp.prototype = this.prototype
                fbind.prototype = new Ftemp()
                // 将fbind.prototype绑定到Ftemp的实例上
                // 这样fbind作为构造函数时,fbind的实例能继承Ftemp实例上的属性和Ftemp原型链上的属性
    
                return fbind
                // 返回一个新的函数
            }
    
            const resFn = fn._bind(obj, 'man')
            const res = resFn('hangzhou')
            const res2 = new resFn('chongqing')
            const xx = res2.go()
            console.log(res, 'res')
            console.log(res2, 'res2')
            console.log(xx, 'xx')
        </script>
    </body>
    </html>
    

    new命令的模拟实现

    • new命令的作用
      • 执行构造函数
      • 返回实例对象
    • 构造函数返回值
      • return 后跟一个对象,new命令返回这个对象
      • return 后跟一个简单数据类型,new命令会不管这个值,返回this对象
    • 继承相关
      • 构造函数中的属性和方法,都是直接生成在实例上的,实例之间不共享,造成资源浪费
      • 实例能继constructor.prototype上的属性和方法,多个实例可以共享,修改则相互影响
    • arguments
      • 使用argumetns的好处是在参数个数不确定的情况下,获取任意多的参数
    • 手写原理
      • 1.新建一个空对象
      • 2.将空对象的隐式原型指向构造函数的显示原型
      • 3.将构造函数中的this绑定到空对象上
      • 4.执行构造函数
      • 5.判断返回值,如果构造函数return一个对象,就返回构造函数返回的对象,否则返回空对象即this对象
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <script>
            function Constructor(name, age) {
                this.name = name
                this.age = age
                return this         
            }
    
            function _new() {
                const obj = {}
                // 第一步
                // 新建一个空对象
                // 相当于 const obj = new Object()
    
                const paramsConstructor = Array.prototype.shift.call(arguments)
                // 将arguments转化成数组,并且取除数组中的第一个元素,即传入的构造函数
                // 相当于 ([]).prototype.shift.call(arguments)
                // 相当于 ([]).shift.call(arguments) => 因为数组实例是继承了Array.prototype上的属性和方法
                // 相当于 Array.prototype.shift.apply(arguments) => call 和 apply 都可以
                // 注意:
                // push unshift pop shift都会改变原数组
                // push unshift 返回值是操作后,数组的长度
                // pop shift 返回值是添加或者删除的元素
    
                obj.__proto__ = paramsConstructor.prototype
                // 第二步
                // 将 ( 空对象的隐式原型 ) 指向 ( 构造函数的显示原型 )
                // 这样空对象就可以继承构造函数prototype上的属性和方法
    
                // 注意:
                    // const obj = {} 和 obj.__proto__ = paramsConstructor.prototype
                    // 可以简写为:const obj = Object.create(paramsConstructor.prototype)
                    // b = Object.create(a)作用是以参数对象a为原型,生成实例对象b - 即可以用一个对象创建实例对象
    
    
                const res = paramsConstructor.apply(obj, arguments)
                // 第三步
                // 将构造函数中的this绑定到空对象上,并执行构造函数
                // 注意:
                // 这里是argumets是去除了构造函数参数后的,剩余参数的集合
                // _new(constructor, p1, p2, ...) 
    
                return /Object|Function/.test(Object.prototype.toString.call(res)) ? res : obj
                // 如果构造函数的返回值
                    // 是对象,就返回这个对象
                    // 是原始类型的值,就返回this对象,即空对象
            }
    
            const instance = _new(Constructor, 'woow_wu7', 20)
            console.log(instance, 'instance')
        </script>
    </body>
    </html>
    

    Debounce 防抖函数

    • 功能:
      • 延时执行,如果在延时的时间内多次触发,则从新计时
      • 可以获取event事件对象作为参数和传入其他参数
      • 第一点击立即触发,而不是延时执行
      • 可以取消debounce的执行
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    <body>
        <div id="debounce">Debounce</div>
        <div id="cancel" style="margin-top: 20px">Debounce-cancel</div>
        <script>
            const fn = () => {
                console.log('fn running')
            }
            // 传入debounce需要延时执行的函数fn
    
            /**
             * @param {function} fn 需要debounce防抖函数处理的函数
             * @param {number} delay 定时器延时的时间
             * @param {boolean} immediate 是否立即执行
             */
            function debounce(fn, delay, immediate) {
                let timer = null
    
                debounce.cancel = () => {
                    console.log('cancel running')
                    clearTimeout(timer)
                }
                // 可以手动取消debounce的执行
    
                return (...args) => {
                    if (immediate && !timer) {
                        // 立即执行的情况
                            // immediate = true
                            // timer = false
                            // 满足这两个条件就立即执行
                        fn.apply(this, args)
                    }
                    if (timer) {
                        console.log('timer exist')
                        clearTimeout(timer)
                        // timer存在,就清除定时器
                    }
                    timer = setTimeout(() => {
                        if (immediate) {
                            // 立即执行条件下
                                // 第一次执行
                                    // 第一次立即执行,则在延时的时间内不触发第二次
                                // 第二次执行
                                    // immediate = false,则不再进入该判断条件了
                            immediate = false
                            return
                        }
                        console.log(args[0].target, 'e.target')
                        fn()
                    }, delay)
                }
            }
    
    
            const button = document.getElementById('debounce')
            const buttonCancel = document.getElementById('cancel')
            button.addEventListener('click', debounce(fn, 2000, true), false)
            buttonCancel.addEventListener('click', debounce.cancel, false)
        </script>
    
    </body>
    </html>
    

    Throttle 节流函数

    • 功能:
      • 每隔一段时间,只执行一次
      • 即在外层函数添加标志位,根据标志位的情况决定是否进入闭包
    基础版
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <div id="throttle">Throttle</div>
        <script>
            const fn = () => {
                console.log('fn running')
            }
            function throttle(fn, delay) {
                let isRun = true // 标志位
                let timer = null
                return (...args) => {
                    // 标志位是false,就return
                    if (!isRun) {
                        return
                    }
                    isRun = false
                    timer = setTimeout(() => {
                        fn.apply(this, args)
                        isRun = true // 执行完标志位改为true,则下次点击又可以执行了
                        clearTimeout(timer) // 清除定时器
                    }, delay)
                }
            }
            const button = document.getElementById('throttle')
            button.addEventListener('click', throttle(fn, 1000), false)
        </script>
    
    </body>
    </html>
    
    时间戳版
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <div id="throttle">Throttle</div>
        <script>
            const fn = () => {
                console.log('fn running')
            }
            function throttle(fn, delay) {
                let previous = null
                return (...args) => {
                    const now = + new Date()
                    // + new Date() 获取现在的时间戳,即距离1970.1.1 00:00:00的毫秒数字
                    // 注意:单位是毫秒数,和定时器的第二个参数吻合,也是毫秒数,虽然这里没有定时器
                    // ( + ) 可以把任意类型的数据转成数值,只有两种可能,即 ( 数值 ) 和 ( NaN )
                    // + new Date() === new Date().valueOf() === new Date().getTime()
                    if (now - previous > delay) {
                        fn.apply(this, arguments)
                        previous = now // 执行完fn,同步时间,用于下一次计算
                        // 第一次:now - previous > delay是true,所以立即执行一次
                        // 然后 previous = now
                        // 第二次:第二次能进来的条件就是差值毫秒数超过delay毫秒
                        // 这样频繁的点击时,就能按照固定的频率执行,当然是降低了频率
                    }
                }
            }
            const button = document.getElementById('throttle')
            button.addEventListener('click', throttle(fn, 1000), false)
        </script>
    </body>
    </html>
    

    数组乱序

    • sort
      • 默认会按照字典进行排序,数字会先被转成字符串,在排序
      • <font color=red>改变原数组,没有返回值</font>
      • 2021/03/18更正,sort改变原数组,有返回值,返回值就是排序过后的数组即被改变过后的数组
      • 参数
        • 可以没有
        • 也可以是一个函数(自定义方式排序就接受函数为参数),参数函数又有两个参数
          • 返回值大于0,表示比较的第一个成员排在第二个成员的后面 (a-b>0表示升序
          • 返回值小于0,表示比较的第一个成员排在第二个成员的前面(a-b<0表示降序
          • 返回值等于0,位置不变
    • 乱序的应用
      • 换一批
      • 猜你喜欢
      • 中奖方案
    方法1
    arr.sort(() => Math.random() - .5)
    
    const arr = [1, 3, 7, 2, 5, 4, 6]
    arr.sort(() => Math.random() - .5)
    // 因为:Math.random() 的值得范围区间是 [0, 1)
    // 所以:Math.random() - 0.5  ===> 大于0或小于0的概率都是50%
        // Math.random()参数参数的返回值:
        // 函数返回值大于0,表示两个比较的成员,第一个排在第二个的后面
        // 函数返回值小于0,表示两个比较的成员,第一个排在第二个的前面
        // 函数返回值等于0,表示两个比较的成员,位置不变
        // 当小数是0点几时,可以省略前面的0
    console.log(arr, 'arr')
    
    
    
    ----
    方法1存在的问题:
    1. 概率不均等
    2. 造成概率不均等的原因:
        - 因为sort()方法底层因为不同浏览器实现方式不一样,v8中
            - 当数组长度小于10时,用的是插入排序
            - 否则,使用的是插入排序和快速排序的混合排序
        - 插入排序就会造成概率不均等
            - 因为把无序部分的元素插入到有序部分中时,如果一旦找到了位置,剩下的元素就没有在和缓存的值就行比较了
            - 即没有机会比较所有元素,造成概率不均等,就得不到完全随机的结果
    
    3. 如何知道sort()排序造成概率不均等呢?可以用下面的方法统计
    const countArr = [0, 0, 0, 0, 0] // 用来存放数组arr最后一个位置,各数字出现的次数
    for (let i = 0; i < 100000; i++) {
      const arr = [0, 1, 2, 3, 4]
      arr.sort(() => Math.random() - .5) // 随机打乱
      countArr[arr[4]]++
      // arr[4] 的可能值是 0 1 2 3 4
      // 如果出现过一个值,就在countArr相对应的位置加1,用来统计各数字出现的次数
    }
    console.log(countArr)
    // [24977, 6951, 20999, 18784, 28289]
    // 从结果中可以看出:arr数组的最后一个位置,出现1的概率明显要小很多,即  arr[4] = 1  的概率明显小很多
    // 其他位置上也会得出相应的结果 arr[x] = 1 的概率都比其他数字出现的概率小
    
    
    
    ----
    直接插入排序复习
    
    插入排序
    - 思想:将一个数组分成有序部分和无序部分,将无序部分的一个值插入到有序部分,有序部分仍然有序 (联想打牌时插牌)
    - 原理:
        - 1. 将数组分成两个部分,左边是有序部分(初始成员个数为1),右边是无序部分(下标从1开始,有序数组中取了一个)
        - 2. 从右边的无序部分依次取出一个值,插入到有序部分,直到取完无序部分
        - 3. 如何找有序部分的插入位置?
            1. 先缓存需要插入的无序部分的值,用一个变量来缓存 let temp = arr[i]
            2. 从有序部分的最后位置找(arr[i-1]),如果arr[i-1] > arr[i] 则该元素往后移动一位
            3. 如果有序该位置仍然比需要插入的值大,有序中该位置的值,也后移动一位,直到 j>=0
    
    const arr = [1, 4, 3, 2]
    const insert_sort = (arr) => {
        for(let i = 1, len = arr.length; i < len; i++) { // 循环数组的无序部分,从1开始,因为假设初始化时有序部分有一个元素
            let temp = arr[i] // 缓存需要插入有序部分的这个无序部分的值,因为有序部分可能会往后移动位置,将其覆盖
            let j = i - 1 
            // j = i - 1 表示有序部分的最后一个元素的位置,有序部分从最后的位置依次往前查找需要插入的位置
            // 这里采用有序部分从后往前寻找,也可以从前往后找
            while(j >= 0 && arr[j] > temp) { 
            // 有序部分循环条件,如果有序的位置的值比无序中的大并且要保证j>=0,就把有序的值往后移动一位
                arr[j+1] = arr[j] // 有序该位置值大于temp,则往后移动一位
                j-- // 依次往前查找
            }
            arr[j+1] = temp // 循环完后,j+1就是需要插入的位置,因为条件是大于temp,不满足时,j+1就是要插入的位置
        }
        return arr // 最后返回数组
    }
    console.log(insert_sort(arr))
    
    ----
    乱序改进版
    
    Fisher–Yates
    
    1. 使用sort(() =>  Math.random() - .5)存在的问题就是不能完全实现全部元素的比较,造成概率不均等
    2. 为什么叫 Fisher–Yates 呢?
      - 因为这个算法是由 Ronald Fisher 和 Frank Yates 首次提出的。
    3.shuffle:是洗牌的意思
    
    4.Fisher–Yates的原理:
    - 首先选取(数组最后一个位置)的元素和(数组长度随机位置)上的元素交换
    - 接着选取(数组倒数第二个位置)的元素和(除去最后一个位置剩下的数组位置,即倒数第二到第一个元素)上的元素交换
    - 重复以上步骤,直到length >= 1
    
    5.代码
    // 数组乱序
    const arr = [1, 2, 3, 4, 5, 6]
    function shuffle(arr) {
        let length = arr.length
        while(length > 1) {
            const pivot = Math.floor(Math.random() * length--) 
            // 1. 注意这里是先赋值,再减 1,即Math.floor(Math.random() * 6) 
            // (Math.random() * 6的区间[0, 6)
            // Math.floor(Math.random() * 6) 区间是 [0-5]
    
            // 2. 语句以分号结尾,一个分号就表示一个语句结束。所以这里赋值语句后面记得加分号
            // 3. ;[arr[pivot], arr[length]] = [arr[length], arr[pivot]]这里条语句前加上分号,因为前面也是语句
            // 在小括号开头,或者中括号开头的语句,前面的语句末尾需要加分号,或者加到本条语句前面
            //4. const pivot = Math.floor(Math.random() * length--); 这样也行
            ;[arr[pivot], arr[length]] = [arr[length], arr[pivot]] // 这里的length是减1之后的length
            // const temp = arr[length]
            // arr[length] = arr[pivot]
            // arr[pivot] = temp
        }
        return arr
    }
    console.log(shuffle(arr))
    

    数组扁平化

    • Array.prototype.flat(拉平的层数)
      • 参数
        • 整数,表示拉平的层数
        • Infinity:表示任意层数的数组都拉平,都展开成一元数组
      • 返回值:
        • 一个新数组,不改变原数组
    • 前置知识
      • concat
        • 作用:用于多个数组的合并,将新数组的成员添加到原数组的尾部
        • 返回值:新的数组
        • 不改变原数组
    方法1
    Array.prototype.flat(Infinity)
    
    -------
    2021/03/18 更新如下
    Array.prototype.flat.call(arr, Infinity)
    
    方法2
    递归 - recursive
    
    const arr = [1, [2, [3, 4, 5, [6]]], 7, 8]
    function flat(arr) {
      let result = []
      for (let i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
          // 如果每一项还是数组,就递归,最终把数据都收集到result中
          result = result.concat(flat(arr[i]))
        }
        else { 
          // 递归结束条件
          result.push(arr[i])
        }
      }
      return result
    }
    const res = flat(arr)
    console.log(res, 'res')
    
    3. toString
    - 如果数组元素都是数值(注意所有的数组中的所有元素都要是数字),可以使用toString()
    - 然而这种方法使用的场景却非常有限,如果数组是 [1, '1', 2, '2'] 的话,这种方法就会产生错误的结果。
    
    
    const arr = [1, [2, 3, [4, [5]]]]
    function flat(arr) {
        return arr.toString().split(',').map(item => +item)
        //  arr.toString()数组的字符串形式,会展平。      =>  1,2,3,4,5
        // arr.toString().split(',')以,为分隔符,将字符串转成数组  ["1", "2", "3", "4", "5"]
        // +item相当于Number(item)
    }
    const res = flat(arr)
    console.log(res)
    
    4. 
    reduce((累积变量, 当面变量,当前位置index, 原数组), 累计变量的初始值)
    - 参数
        - 函数
            - 参数
            - 累积变量prev:如果reduce()没有第二个参数,就是数组的第一个元素
            - 当前变量next:如果reduce()没有第二个参数,就是数组的第二个元素
        - 累计变量初始值
            - 累计变量初始值,即赋值给prev,此时next就是数组的第一个成员
    
    const arr = [1, [2, [3, 4, 5, [6]]], 7, 8, 9]
    function flat(arr) {
      return arr.reduce((prev, next) => {
        return prev.concat(Array.isArray(next) ? flat(next) : next)
        // 返回拼接后的数组,如果当前变量是数组,recursive执行
      }, [])
      // 这里指定了初始值是 []
      // prev = []
      // next = 1,因为prev初始值是空数组,所以next的初始值是 1
    
      // reduce((accumulate, currentValue, index, arr) => {....}, [])
      // 第一个参数:是一个函数
        // 第一个参数:累积变量,默认是数组的第一个元素
        // 第二个参数:当前变量,默认是数组的第二个元素
        // 第三个参数:当前位置(当前变量在数组中的位置)
        // 第四个参数:原数组
      // 第二个参数:累积变量的初始值,注意如果指定了初始值,那么当前变量就从数组的第一个元素开始
    }
    const res = flat(arr)
    console.log(res, 'res')
    // [1, 2, 3, 4, 5, 6, 7, 8, 9] "res"
    
    
    5. 使用 ...展开运算符
    
    let arr = [1, [2, [3, 4]]];
    function flat(arr) {
      while (arr.some(item => Array.isArray(item))) { // 循环判断是不是数组,是数组就展开
        arr = [].concat(...arr); // 每次都扁平一层
      }
      return arr;
    }
    console.log(flat(arr))
    

    XMLHttpRequest

    • 如何获取response???
      • xhr.response
      • xhr.responseText
      • xhr.responseXML
      • xhr.responseText
        • 在 xhr.responseType = 'text' , '',不设置时,xhr实例对象上才有此属性,此时才能调用
      • xhr.response
        • 在 xhr.responseType = 'text' ,'' 时,值是 ( '' )
        • 在 xhr.resposneType 是其他值时,值是 ( null )
    • xhr.responseType
      • text
      • document
      • json
      • blob
      • arrayBuffer
    const xhr = new XMLHttpRequest()
    // new 命令总是返回一个对象,要么是this对象,要么是return后面跟的对象
    // new 调用的是构造函数,说明XMLHttpRequest是一个构造函数
    
    (1) xhr.open()
    - 初始化 HTTP 请求参数,比如url,http请求方法等,但并 ( 不发送请求 )
    - xhr.open() 方法主要供 xhr.send() 方法使用
    
    xhr.open(method, url, async, username, password)
    - 参数
      - method:http请求的方法,包括 GET POST HEAD
      - url:请求的地址
      - async:是否异步
        - true,默认值,异步请求,通常需要调用 onreadystatechange() 方法
        - false,对 send() 方法的调用将阻塞,直到响应完全接收
    
    (2) xhr.send()
    - 发送一个http请求
    
    xhr.send(body)
    - get请求:get请求的参数可以直接写在 open() 方法中
    - post请求:post请求的参数写在 send() 方法中
      - 注意:
        - body参数的数据类型会影响 requestHeader 中的 Content-Type 的默认值,如何手动指定则会覆盖默认值
        - 如果data是 Document 类型,同时也是HTML Document类型,则content-type默认值为text/html;charset=UTF-8;否则为application/xml;charset=UTF-8;
        - 如果data是 DOMString 类型,content-type默认值为text/plain;charset=UTF-8;
        - 如果data是 FormData 类型,content-type默认值为multipart/form-data; boundary=[xxx]
        - 如果data是其他类型,则不会设置content-type的默认值
    
    (3) xhr.setRequestHeader()
    - 指定一个http请求的头部,只有在 readState = 1 时才能调用
    - setRequestHeader可以调用的时机
      - 1. 在 readyStaet = 1 时
      - 2. 在 open() 方法之后,send() 方法之前
      - 3. 其实 1 2 是一个意思
    
    xhr.setRequestHeader('name', 'value')
    - 参数
      - name:头部的名称
      - value:头部的值
    - 注意
      - setRequestHeader() 方法可以 ( 多次调用 ) ,值不是 ( 覆盖override ) 而是 ( 追加append )
      - setRequestHeader() 只有在 readyState = 1 时才能调用,即 open() 方法之后,send() 方法之前
      
    (4) xhr.getResponseHeader()
    - 指定http响应头部的值
    
    (5) xhr.abort()
    - 取消当前响应,关闭连接并且结束任何未决的网络活动
    - xhr.abort()会将 readyState 重置为0
    - 应用:取消请求,在请求耗时太长,响应不再有必要时,调用该方法
    - abort:是终止的意思
    
    (6) xhr.onreadystatecange()
    - 在 readyState 状态改变时触发
    - xhr.onreadystatechange() 在 readyState = 3 时,可能多次调用
    - onreadystatechange 都是小写
    - readyState 驼峰
    
    readyState状态
    0 UNSENT ------------- xhr对象成功构造,open() 方法未被调用
    1 OPEND  ------------- open() 方法被调用,send() 方法未被调用,setRequestHeader() 可以被调用
    2 HEADERS_RECEIVED --- send() 方法已经被调用,响应头和响应状态已经返回
    3 LOADING ------------ 响应体 ( response entity body ) 正在下载中,此状态下通过 xhr.response 可能已经有了响应数据
    4 NODE   ------------- 整个数据传输过程结束,不管本次请求是成功还是失败
    
    (7) xhr.onload
    - 请求成功时触发,此时 readyState = 4
    - 注意:重点!!!
      - 1.除了在   xhr.onreadystatechange 指定的回调函数的 readyState = 4 时取值
      - 2.还可以在 xhr.onload事件中取值
          xhr.onload = function() {
            // 请求成功
            if (xhr.status === 200 ) {
              // do successCallback
            }
          }
      - 3.判断 xhr.status === 200 是有坑的,因为成功时返回的状态码不只有200,下面的写法更靠谱
          xhr.onload = function () {
            //如果请求成功
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
              // 304 not modified 资源未被修改 协商缓存
              // 2开头的状态码,表示请求成功
              //do successCallback
            }
          }
    
    (8) xhr.timeout
    - 设置过期时间
    - 问题1:请求的开始时间怎么确定?是 ( xhr.onloadstart ) 事件触发的时候,也就是xhr.send()调用的时候
    - 解析:因为xhr.open()只是创建了链接,当并没有真正传输数据,只有调用xhr.send()时才真正开始传输
    - 问题2:什么时候是请求结束?
    - 解析:( xhr.loadend ) 事件触发时结束
    
    (9)  xhr.onprogress 下载进度信息
    (10) xhr.upload.onprogress 上传进度信息
    xhr.upload.onprogress = function(e) {
        if ( e.lengthComputable ) {
          const present = e.loaded / e.total * 100;
        }
    }
    
    • XMLHttpRequest请求案例
    XMLHttpRequest请求案例
    
    ----
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <button id="buttonId">点击,请求数据</button>
      <script>
        const button = document.getElementById('buttonId')
        button.addEventListener('click', handleClick, false)
    
        function handleClick() {
          const xhr = new XMLHttpRequest()
          xhr.open('GET', 'http://image.baidu.com/channel/listjson?pn=0&rn=30&tag1=明星&tag2=全部&ie=utf8', true) // open()方法
          xhr.setRequestHeader('Content-Type', 'application/json') // setRequestHeader必须在open()方法后,send()方法前调用,即 readyState === 1时
          xhr.responseType = 'text'
          xhr.timeout = 10000
          xhr.onreadystatechange = function () {
            if (xhr.readyState === 4 && xhr.status === 200) {
              // 这里通过 this 代替 xhr 其实是一样的
              // 因为 this 在运行时确定指向,xhr实例在调用onreadystatechange方法,所以this指向xhr实例
              console.log(JSON.parse(this.responseText)) // 等价于 console.log(JSON.parse(xhr.responseText))
            }
          }
          xhr.onload = function () {
            if ((xhr.status >= 200 && xhr.status < 300) || (xhr.status === 304)) {
              console.log(JSON.parse(xhr.responseText), 'xhr.onload是在请求完成时触发的回调')
            }
          }
          xhr.send() // 发送请求
        }
      </script>
    </body>
    </html>
    
    image

    资料

    call,apply 模拟实现 https://github.com/mqyqingfeng/Blog/issues/11
    bind模拟实现 https://juejin.im/post/6844903476623835149
    new模拟实现 https://github.com/mqyqingfeng/Blog/issues/13
    new 模拟实现
    1.https://github.com/mqyqingfeng/Blog/issues/13
    2.https://segmentfault.com/a/1190000014452865
    3.https://segmentfault.com/a/1190000015276659
    Throttle Debounce 我的掘金 https://juejin.im/post/6844904054330490894
    数组乱序 https://juejin.im/post/6844903863812620296#heading-2
    数组扁平化 https://github.com/mqyqingfeng/Blog/issues/36

    相关文章

      网友评论

          本文标题:[深入20] 手写函数

          本文链接:https://www.haomeiwen.com/subject/nwewgltx.html