JavaScript进阶:手写代码挑战(一)

前端青山 2024-10-27 10:35:02 阅读 82

​🌈个人主页:前端青山

🔥系列专栏:JavaScript篇

🔖人终将被年少不可得之物困其一生

依旧青山,本期给大家带来JavaScript篇专栏内容:JavaScript手写代码篇

#1024程序员节|征文#

在现代Web开发中,JavaScript 是不可或缺的编程语言。掌握其核心功能和原理对于开发者至关重要。本文通过手写实现JavaScript的一些关键功能和算法,帮助读者深入理解其工作原理,提升编程技能。无论你是初学者还是有经验的开发者,都能从中受益。

目录

1、手写一个Promise

2、手写call,apply,bind实现详解

4、手写一个防抖的实现

5、手写一个节流的实现

6、手写ajax  

7、手写JSONP的原理和实现

8、手写深拷贝

9、bind

10 call

11 apply


1、手写一个Promise

<code>/**

* 自定义Promise函数模块

*/

(function () {

  const PENDING = 'pending'

  const RESOLVED = 'resolved'

  const REJECTED = 'rejected'

  /**

    * Promise构造函数

    * @param {*} executor 执行器函数

    */

  function Promise(executor) {

      const self = this

      // 给promise对象指定status属性,初始值为pending

      self.status = PENDING

      // 给promise对象指定一个用于存储结果数据的属性

      self.data = undefined

      // 每个元素的结构 { onResolved(){}, onRejected() }

      self.callbacks = []

      function resolve(value) {

          // 此处做判断,使得promise的状态只能修改一次

          if (self.status === PENDING) {

              // 将状态改为 resolved

              self.status = RESOLVED

              // 保存value数据

              self.data = value

              // 如果有待执行的callback函数,立即异步执行回调

              if (self.callbacks.length > 0) {

                  setTimeout(() => { // 表示在异步队列中执行

                      self.callbacks.forEach(callbacksObj => {

                          callbacksObj.onResolved(value)

                      })

                  }, 0);

              }

          }

      }

      function reject(reason) {

          // 此处做判断,使得promise的状态只能修改一次

          if (self.status === PENDING) {

              // 将状态改为 resolved

              self.status = REJECTED

              // 保存value数据

              self.data = reason

              // 如果有待执行的callback函数,立即异步执行回调

              if (self.callbacks.length > 0) {

                  setTimeout(() => { // 表示在异步队列中执行

                      self.callbacks.forEach(callbacksObj => {

                          callbacksObj.onRejected(reason)

                      })

                  }, 0);

              }

          }

      }

      // 立即执行器

      try {

          executor(resolve, reject);

      } catch (error) { // 如果执行器抛出异常,promise状态变为rejected状态

          reject(error)

      }

  }

  /**

    * Promise原型对象的then方法

    */

  Promise.prototype.then = function (onResolved, onRejected) {

      const self = this

      onResolved = typeof onResolved === 'function' ? onResolved : value => value

      // 指定默认的失败的回调(实现错误/异常穿透的关键点)

      onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }

      return new Promise((resolve, reject) => {

          /**

            * 处理onResolve和onRejected函数

            * @param {*} callback

            */

          function resolvePromise(callback) {

              try {

                  const result = callback(self.data)

                  if (result instanceof Promise) {

                      result.then(resolve, reject)

                  } else if (result !== null && (typeof result === 'object' || typeof result === 'function')) {

                      // 拿到result.then

                      const then = result.then;

                      if (typeof then === 'function') {

                          then(resolve, reject)

                      } else {

                          resolve(then)

                      }

                  } else {

                      resolve(result)

                  }

              } catch (error) {

                  reject(error)

              }

          }

          if (self.status === RESOLVED) {

              setTimeout(() => {

                  /**

                    * 1. 如果抛出异常,return 的 promise就会失败,reason就是error

                    * 2. 如果回调函数返回不是promise,return的promise就会成功,value就是返回值

                    * 3. 如果回调函数返回的是一个promise,return的promise的结果就是这个promise的结果,value就是返回值

                    */

                  resolvePromise(onResolved)

              }, 0);

          } else if (self.status === REJECTED) {

              setTimeout(() => {

                  resolvePromise(onRejected)

              }, 0);

          } else {

              self.callbacks.push({

                  onResolved(value) {

                      resolvePromise(onResolved)

                  },

                  onRejected(reason) {

                      resolvePromise(onRejected)

                  }

              })

          }

      })

  }

  /**

    * Promise原型对象的catch方法

    */

  Promise.prototype.catch = function (onRejected) {

      return this.then(undefined, onRejected)

  }

  /**

    * Promise函数对象的resolve方法

    *

    * 返回一个指定结果的成功的promise

    */

  Promise.resolve = function (value) {

      return new Promise((resolve, reject) => {

          if (value instanceof Promise) {

              value.then(resolve, reject)

          } else {

              resolve(value)

          }

      })

  }

  /**

    * Promise函数对象的reject方法

    */

  Promise.reject = function (reason) {

      return new Promise((resolve, reject) => {

          reject(reason)

      })

  }

  /**

    * Promise函数对象的all方法

    */

  Promise.all = function (promises) {

      // 判断数组

      if (!(promises instanceof Array)) {

          return Promise.reject(new Error('params must be a Array!'))

      }

      // 用来保存所有数据成功value的数组

      const resultArray = new Array(promises.length)

      // 用来保存成功数量的计数

      let resultCount = 0

      return new Promise((resolve, reject) => {

          // 遍历获取每个promise的结果

          promises.forEach((p, index) => {

              Promise.resolve(p).then(value => {

                  resultCount++

                  resultArray[index] = value

                  // 如果全部都成功了,将return的promise变为成功

                  if (resultCount === promises.length) {

                      // 表示成功

                      resolve(resultArray)

                  }

              }, reason => {

                  reject(reason)

              })

          })

      })

  }

  /**

    * Promise函数对象的race方法

    */

  Promise.race = function (promises) {

      // 判断数组

      if (!(promises instanceof Array)) {

          return Promise.reject(new Error('params must be a Array!'))

      }

      return new Promise((resolve, reject) => {

          // 遍历数组

          promises.forEach((p, index) => {

              Promise.resolve(p).then(value => {

                  resolve(value)

              }, reason => {

                  reject(reason)

              })

          })

      })

  }

  // ---------------------------------------实现自己的方法---------------------------------------

  /**

    * 返回一个promise对象,它在指定的时间后才确定成功结果

    * @param {*} value

    * @param {*} time

    */

  Promise.resolveDelay = function (value, time) {

      return new Promise((resolve, reject) => {

          setTimeout(() => {

              if (value instanceof Promise) {

                  value.then(resolve, reject)

              } else {

                  resolve(value)

              }

          }, time);

      })

  }

  /**

    * 返回一个promise对象,它在指定的时间后才确定失败结果

    * @param {*} reason

    * @param {*} time

    */

  Promise.rejectDelay = function (reason, time) {

      return new Promise((resolve, reject) => {

          setTimeout(() => {

              reject(reason)

          }, time);

      })

  }

  // 向外暴露函数

  window.Promise = Promise

})(window)

2、手写call,apply,bind实现详解

call 接收多个参数,第一个为函数上下文也就是this,后边参数为函数本身的参数。

      let obj = {

          name: "一个"

      }

      function allName(firstName, lastName) {

          console.log(this)

          console.log(`我的全名是“${firstName}${this.name}${lastName}”`)

      }

      // 很明显此时allName函数是没有name属性的

      allName('我是', '前端') //我的全名是“我是前端” this指向window

      allName.call(obj, '我是', '前端') //我的全名是“我是一个前端” this指向obj

复制代码

apply

apply接收两个参数,第一个参数为函数上下文this,第二个参数为函数参数只不过是通过一个数组的形式传入的。

allName.apply(obj, ['我是', '前端'])//我的全名是“我是一个前端” this指向obj

复制代码

bind

bind 接收多个参数,第一个是bind返回值返回值是一个函数上下文的this,不会立即执行。

      let obj = {

          name: "一个"

      }

      function allName(firstName, lastName, flag) {

          console.log(this)

          console.log(`我的全名是"${firstName}${this.name}${lastName}"我的座右铭是"${flag}"`)

      }

      allName.bind(obj) //不会执行

      let fn = allName.bind(obj)

      fn('我是', '前端', '好好学习天天向上')

      // 也可以这样用,参数可以分开传。bind后的函数参数默认排列在原函数参数后边

      fn = allName.bind(obj, "你是")

      fn('前端', '好好学习天天向上')

复制代码

接下来搓搓手实现call、apply和bind

实现call

    let Person = {

          name: 'Tom',

          say() {

              console.log(this)

              console.log(`我叫${this.name}`)

          }

      }

      // 先看代码执行效果

      Person.say() //我叫Tom

      Person1 = {

          name: 'Tom1'

      }

      // 我们尝试用原生方法call来实现this指向Person1

      Person.say.call(Person1) //我叫Tom1

复制代码

通过第一次打印执行和第二次打印执行我发现,如果Person1有say方法那么Person1直接执行Person1.say() 结果就是我是Tom1,是的call就是这么实现的。 再看代码

      Function.prototype.MyCall = function(context) {

          //context就是demo中的Person1

          // 必须此时调用MyCall的函数是say方法,那么我们只需要在context上扩展一个say方法指向调用MyCall的say方法这样this

          console.log(this)

          context.say = this //Mycall里边的this就是我们虚拟的say方法

          context.say()

      }

      // 测试

      Person.say.MyCall(Person1)//我叫Tom1

复制代码

perfect!爆棚的满足感!不过拿脚趾头想想也不会这么简单,继续完善 我们自己找茬 1、call支持多个参数,有可能一个也不没有 2、考虑多参数时要把参数传给扩展方法。 3、给上下文定义的函数要保持唯一不能是say 4、扩展完我们需要吧自定义函数删除 接下来针对找茬问题一一解决

      let Person = {

          name: 'Tom',

          say() {

              console.log(this)

              console.log(`我叫${this.name}`)

          }

      }

      Person1 = {

          name: 'Tom1'

      }

      //如果没有参数

      Person.say.call()

复制代码

没有指定this,this指向window

我们也要这样

      Function.prototype.MyCall = function(context) {

          // 如果没有参数我们参考call的处理方式

          context = context || window

              //context就是demo中的Person1

              // 必须此时调用MyCall的函数是say方法,那么我们只需要在context上扩展一个say方法指向调用MyCall的say方法这样this

          context.say = this //Mycall里边的this就是我们虚拟的say方法

          context.say()

      }

      Person.say.MyCall()

复制代码

没毛病! 继续解决

// 找茬2:我们默认定义context.say = this fn如果已经被占用 嘎嘎 sb了。 不怕 搞定它

      // say需要是一个唯一值 是不是突然想到es6的新类型 Symbol   fn = Symbol() 不过我们装逼不嫌事大 都说自己实现了

      function mySymbol(obj) {

          // 不要问我为什么这么写,我也不知道就感觉这样nb

          let unique = (Math.random() + new Date().getTime()).toString(32).slice(0, 8)

              // 牛逼也要严谨

          if (obj.hasOwnProperty(unique)) {

              return mySymbol(obj) //递归调用

          } else {

              return unique

          }

      }

//接下来我们一并把多参数和执行完删除自定义方法删除掉一块搞定

      Function.prototype.myCall1 = function(context) {

          // 如果没有传或传的值为空对象 context指向window

          context = context || window

          let fn = mySymbol(context)

          context.fn = this //给context添加一个方法 指向this

          // 处理参数 去除第一个参数this 其它传入fn函数

          let arg = [...arguments].slice(1) //[...xxx]把类数组变成数组,arguments为啥不是数组自行搜索 slice返回一个新数组

          context.fn(...arg) //执行fn

          delete context.fn //删除方法

      }

       

      let Person = {

          name: 'Tom',

          say(age) {

              console.log(this)

              console.log(`我叫${this.name}我今年${age}`)

          }

      }

      Person1 = {

          name: 'Tom1'

      }

      Person.say.call(Person1,18)//我叫Tom1我今年18

复制代码

测试结果相当完美!

实现apply

接下来apply就简单多了,只有多参数时第二个参数是数组,就不一步步细说了。

      Function.prototype.myApply = function(context) {

          // 如果没有传或传的值为空对象 context指向window

          if (typeof context === "undefined" || context === null) {

              context = window

          }

          let fn = mySymbol(context)

          context.fn = this //给context添加一个方法 指向this

              // 处理参数 去除第一个参数this 其它传入fn函数

          let arg = [...arguments].slice(1) //[...xxx]把类数组变成数组,arguments为啥不是数组自行搜索 slice返回一个新数组

          context.fn(arg) //执行fn

          delete context.fn //删除方法

      }

复制代码

实现bind

这个和call、apply区别还是很大的,容我去抽根烟回来收拾它 还是老套路先分析bind都能干些什么,有什么特点 1、函数调用,改变this 2、返回一个绑定this的函数 3、接收多个参数 4、支持柯里化形式传参 fn(1)(2)

      Function.prototype.bind = function(context) {

          //返回一个绑定this的函数,我们需要在此保存this

          let self = this

              // 可以支持柯里化传参,保存参数

          let arg = [...arguments].slice(1)

              // 返回一个函数

          return function() {

              //同样因为支持柯里化形式传参我们需要再次获取存储参数

              let newArg = [...arguments]

              console.log(newArg)

                  // 返回函数绑定this,传入两次保存的参数

                  //考虑返回函数有返回值做了return

              return self.apply(context, arg.concat(newArg))

          }

      }

      // 搞定测试

      let fn = Person.say.bind(Person1)

      fn()

      fn(18)

3、手写一个instanceOf

实现思路:

首先 instanceof 左侧必须是对象, 才能找到它的原型链

instanceof 右侧必须是函数, 函数才会prototype属性

迭代 , 左侧对象的原型不等于右侧的 prototype时, 沿着原型链重新赋值左侧

复制代码

// [1,2,3] instanceof Array ---- true

// L instanceof R

// 变量R的原型 存在于 变量L的原型链上

function instance_of(L,R){    

  // 验证如果为基本数据类型,就直接返回false

  const baseType = ['string', 'number','boolean','undefined','symbol']

  if(baseType.includes(typeof(L))) { return false }

   

  let RP = R.prototype; //取 R 的显示原型

  L = L.__proto__;       //取 L 的隐式原型

  while(true){           // 无线循环的写法(也可以使 for(;;) )

      if(L === null){   //找到最顶层

          return false;

      }

      if(L === RP){       //严格相等

          return true;

      }

      L = L.__proto__; //没找到继续向上一层原型链查找

  }

}

4、手写一个防抖的实现

!DOCTYPE html>

<html>

<!-- 防抖 -->

<!-- 防抖就是在n秒内 防止连续触发,在n秒内触发了下一次,那就重新计算 -->

<body>

<div id="content"code>

  style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>code>

<script>

  let num = 1;

  let content = document.getElementById('content');

  /**

    *非立即执行版

    **/

  function debounceNoAtOnce(func, wait) {

    let timeout;

    return function () {

      let context = this;

      let args = arguments;

      if (timeout) clearTimeout(timeout);

      timeout = setTimeout(() => {

        func.apply(context, args)

      }, wait);

    }

  };

  /**

    *立即执行版

    **/

  function debounceAtOnce(func, wait) {

    let timeout;

    return function () {

      let context = this;

      let args = arguments;

      debugger

      if (timeout) clearTimeout(timeout);

      let callNow = !timeout;

      timeout = setTimeout(() => {

        timeout = null;

      }, wait)

      if (callNow) func.apply(context, args)

    }

  }

  /**

    *聚合版

    **/

  function debounce(func, wait, immediate) {

    let timeout;

    return function () {

      let context = this;

      let args = arguments;

      if (timeout) clearTimeout(timeout);

      if (immediate) {

        var callNow = !timeout;

        timeout = setTimeout(() => {

          timeout = null;

        }, wait)

        if (callNow) func.apply(context, args)

      } else {

        timeout = setTimeout(function () {

          func.apply(context, args)

        }, wait);

      }

    }

  }

  function count() {

    content.innerHTML = num++;

  };

  content.onmousemove = debounce(count, 1000, true);

</script>

</body>

<script>

</script>

</html>

防抖的目的在于:n秒内点击多少次都算一次+每次点击都重新计算时间

5、手写一个节流的实现

<!DOCTYPE html>

<html>

<!-- 节流 -->

<!-- 节流是为了固定的时间段内只能点击一次 -->

<body>

<div id="content"code>

  style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>code>

<script>

  let num = 1;

  let content = document.getElementById('content');

  /**

    *throttleTime 时间戳版

    **/

  throttleTime = function (func, wait) {

    let previde = 0;

    return function () {

      let nowDate = Date.now();

      if (nowDate - previde > wait) {

        func();

        previde = nowDate;

      }

    }

  }

  /**

    *throttleSet 定时器版

    **/

  throttleSet = function (func, wait) {

    let timeout;

    return function () {

      if (!timeout) {

        timeout = setTimeout(() => {

          timeout = false

          func()

        }, wait)

      }

    }

  }

  function count() {

    content.innerHTML = num++;

  };

  content.onmousemove = throttleSet(count, 1000);

</script>

</body>

<script>

</script>

</html>

节流的目的在于:固定时间段内只能点击一次

应用场景:输入框输入+提交/确定

6、手写ajax  

function createXMLHTTPRequest() {    

                //1.创建XMLHttpRequest对象    

                //这是XMLHttpReuquest对象无部使用中最复杂的一步    

                //需要针对IE和其他类型的浏览器建立这个对象的不同方式写不同的代码    

                var xmlHttpRequest;  

                if (window.XMLHttpRequest) {    

                    //针对FireFox,Mozillar,Opera,Safari,IE7,IE8    

                  xmlHttpRequest = new XMLHttpRequest();    

                    //针对某些特定版本的mozillar浏览器的BUG进行修正    

                    if (xmlHttpRequest.overrideMimeType) {    

                        xmlHttpRequest.overrideMimeType("text/xml");    

                    }    

                } else if (window.ActiveXObject) {    

                    //针对IE6,IE5.5,IE5    

                    //两个可以用于创建XMLHTTPRequest对象的控件名称,保存在一个js的数组中    

                    //排在前面的版本较新    

                    var activexName = [ "MSXML2.XMLHTTP", "Microsoft.XMLHTTP" ];    

                    for ( var i = 0; i < activexName.length; i++) {    

                        try {    

                            //取出一个控件名进行创建,如果创建成功就终止循环    

                            //如果创建失败,回抛出异常,然后可以继续循环,继续尝试创建    

                          xmlHttpRequest = new ActiveXObject(activexName[i]);  

                          if(xmlHttpRequest){  

                              break;  

                          }  

                        } catch (e) {    

                        }    

                    }    

                }    

                return xmlHttpRequest;  

            }  

7、手写JSONP的原理和实现

JSONP实现原理

jsonp,其实就是单纯为了实现跨域请求而创造的一个欺骗(trick)。

虽然,因为同源策略的影响,不能通过XMLHttpRequest请求不同域上的数据(Cross -origin reads)。但是,在页面上引入不同域上的js脚本文件却是可以的(Cross -origin embedding)。因此,在js文件载入完毕之后,触发回调,可以将需要的data作为参数传入 注意,实现方式(需前后端配合)

优点

兼容性好(兼容低版本IE)

缺点

JSONP只支持GET请求,XMLHttpRequest相对于JSONP有着更好的错误处理机制。

实现

1.服务端采用的是Node,服务端处理请求方法如下

router.get('/', function(req, res, next) {

   console.log('收到客户端的请求:', req.query);

   // 传回到客户端的数据

   let data = JSON.stringify({

       'status':200,

       'result':{

           'name':'柳成荫',

           'site':'123456'

      }

  });

   // 获取方法名称 - 这是客户端传过来的方法名参数

   // 因为这个方法名必须是客户端有的,必须要客户端告诉服务端是哪个方法

   let methodName = req.query.callback;

   let methodStr = methodName + '(' + data + ')';

   // 快速结束没有任何数据的响应,客户端会执行这个方法,从而获取到服务端返回的数据

   res.end(methodStr)

});

2.封装JSONP

(function (w) {

       /**

        * jsonp的实现

        * @param {Object}option

        */

       function jsonp(option) {

           // 把success函数挂载在全局的getDate函数上

           w.getData = option.success;

           // 处理url,拼接参数 - 回调方法是getData

           option.url = option.url + '?callback=getData';

           // 创建script标签,并插入body

           let scriptEle = document.createElement('script');

           scriptEle.src = option.url;

           document.body.appendChild(scriptEle);

      }

       // 全局挂载一个jsonp函数

       w.jsonp = jsonp;

  })(window);

   /**

    * 把对象转换成拼接字符串

    * 把形如

      data:{

        "sex":"男",

        "name":"九月"

      }

      转换成sex=男&name=九月

    * @param paramObj 对象参数

    * @param words

    * @returns {string} 字符串

    */

   function getStrWithObject(paramObj,words){

       let resArr = [];

       // 1.转换对象

       for(let key in paramObj){

           let str = key + '=' + paramObj[key];

           resArr.push(str);

      }

       resArr.push(words);

       // 3.数组转换成字符串

       return resArr.join("&");

  }

看似代码没有任何问题。但是,我们测试一下,多次调用jsonp方法。

jsonp({

       url:'http://localhost:3000/',

       data:{

           "sex":"男",

           "name":"九月"

      },

       success:function (data) {

           console.log(data);

           alert(1);

      }

  });

   jsonp({

       url:'http://localhost:3000/',

       data:{

           "sex":"男",

           "name":"九月"

      },

       success:function (data) {

           console.log(data);

           alert(2);

      }

  });

结果会怎么样?的确,还是会执行2次,但是每次执行的都是第二个jsonp方法。这是为什么?

问题出现在这里,多次调用,会发生函数覆盖。

img

解决方案就是让每一次调用函数名不一致,在JS里有很多方法,比如通过随机数、时间戳之类的。这里采用的是随机数,修改如下。

<code>// 0.产生不同的函数名 - 解决了调用多次请求,造成覆盖的问题

// 如果方法名相同,调用多次,都会执行最后一个,出现覆盖现象

let callBackName = 'lcy' + Math.random().toString().substr(2)

+ Math.random().toString().substr(2);

// 把success函数挂载在全局的getDate函数上

w[callBackName] = option.success;

// 处理url,拼接参数

option.url = option.url + '?' + getStrWithObject(option.data,'callback='+callBackName);

好,现在看似完美解决。然而,我们发现,每次调用jsonp方法,都会在body里插入一个script标签。我们并不希望在调用jsonp之后,添加这样一个标签,我们需要在调用完之后,将其移除。

怎么做呢?我们在将success函数挂载在全局时,我们在success外层再套个函数。在这个函数里调用success方法之后,即已经请求到数据之后,我们就去把这个script标签给它移除就行了。修改如下

// 1.函数挂载在全局

w[callBackName] = function(data){

   option.success(data);

   // 删除script标签

   document.body.removeChild(scriptEle);

};

整个过程就是这样,以下是完整代码。

(function (w) {

       /**

        * jsonp的实现

        * @param {Object}option

        */

       function jsonp(option) {

           // 0.产生不同的函数名 - 解决了调用多次请求,造成覆盖的问题

           // 如果方法名相同,调用多次,都会执行最后一个,出现覆盖现象

           let callBackName = 'lcy' + Math.random().toString().substr(2) + Math.random().toString().substr(2);code>

           // 1.函数挂载在全局

           w[callBackName] = function(data){

               option.success(data);

               // 删除script标签

               document.body.removeChild(scriptEle);

          };

           // 2.处理url

           option.url = option.url + '?' + getStrWithObject(option.data,'callback='+callBackName);

           // 3.创建script标签插入body

           let scriptEle = document.createElement('script');code>

           scriptEle.src = option.url;

           document.body.appendChild(scriptEle);

      }

       w.jsonp = jsonp;

  })(window);

   /**

    * 把对象转换成拼接字符串

    * @param paramObj 对象参数

    * @returns {string} 字符串

    */

   function getStrWithObject(paramObj,words){

       let resArr = [];

       // 1.转换对象

       for(let key in paramObj){

           let str = key + '=' + paramObj[key];

           resArr.push(str);

      }

       resArr.push(words);

       // 3.数组转换成字符串

       return resArr.join("&");

  }

8、手写深拷贝

ar a = {

  name: "muyiy",

  book: {

      title: "You Don't Know JS",

      price: "45"

  },

  a1: undefined,

  a2: null,

  a3: 123

}

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

function cloneDeep1(source) {

  var target = {};

  for (var key in source) {

      if (source.hasOwnProperty(key)) {

          if (typeof source[key] === 'object') {

              target[key] = cloneDeep1(source[key]); // 注意这里

          } else {

              target[key] = source[key];

          }

      }

  }

  return target;

}

// 使用上面测试用例测试一下

console.log(a)

var b = cloneDeep1(a);

a.name = "前端进阶";

a.book.price = "55";

console.log(b);

考虑再全一些的深拷贝

function deepClone(obj,hash = new WeakMap()){ //递归实现

  if(obj instanceof RegExp) return new RegExp(obj);

  if(obj instanceof Date) return new Date(obj);

  if(obj === null || typeof obj != "object"){

      // 普通数据类型

      return obj;

  }

  if(hash.has(obj)){

      return hash.get(obj);

  }

  // 下面是数组和对象的判断

  let t = new obj.constructor();

  hash.set(obj,t);

  for(let key in obj){

      // 递归

      if(obj.hasOwnProperty(key)){ //是否是自身的属性

          t[key] = deepClone(obj[key],hash)

      }

  }

  return t;

}

9、bind

function print() {

  console.log(this.name, ...arguments);

}

const obj = {

  name: 'mxin',

};

// Function.prototype.__bind()

console.log('Function.prototype.__bind()');

// 直接调用,返回原函数拷贝,this 指向 obj

const F = print.__bind(obj, 26);

F(178); // mxin, 26, 178

// new 情况

const _obj = new F(145); // undefined, 26, 145

console.log(_obj); // print {}

// Function.prototype.bind()

console.log('Function.prototype.bind()');

const Fn = print.bind(obj, 26);

Fn(178); // mxin, 26, 178

const __obj = new Fn(145); // undefined, 26, 145

console.log(__obj); // print {}

10 call

/**

* 模拟 call

* 使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数

* @param {object} ctx

* @param {...any} args

* @returns {any} 调用 this 的返回值,若无有返回值,则返回 undefined

*/

Function.prototype.__call = function (ctx, ...args) {

  if (typeof this !== 'function') throw new TypeError('Error');

  // 考虑 null 情况,参数默认赋值会无效

  if (!ctx) ctx = window;

  // 将 this 函数保存在 ctx 上

  ctx.fn = this;

  // 传参执行并保存返回值

  const res = ctx.fn(...args);

  // 删除 ctx 上的 fn

  delete ctx.fn;

 

  return res;

};

// ------------------------------ 测试 ------------------------------

function Product(name, price) {

  this.name = name;

  this.price = price;

}

// Function.prototype.__call()

console.log('Function.prototype.__call()');

function Food(name, price) {

  Product.__call(this, name, price);

  this.category = 'food';

}

const food = new Food('cheese', 5);

console.log(food);

// Food {name: "cheese", price: 5, category: "food"}

//   category: "food"

//   name: "cheese"

//   price: 5

//   __proto__:

//     constructor: ƒ Food(name, price)

//     __proto__: Object

// Function.prototype.call()

console.log('Function.prototype.call()');

function Toy(name, price) {

  Product.call(this, name, price);

  this.category = 'toy';

}

const toy = new Toy('car', 10);

console.log(toy);

// Toy {name: "car", price: 10, category: "toy"}

//   category: "toy"

//   name: "car"

//   price: 10

//   __proto__:

//     constructor: ƒ Toy(name, price)

//     __proto__: Object

11 apply

/**

* 模拟 apply

* 调用一个具有给定 this 值的函数,以及以一个数组(或类数组对象)的形式提供的参数

* @param {object} ctx

* @param {} args

*/

Function.prototype.__apply = function (ctx, args) {

  if (typeof this !== 'function') throw new TypeError('Error');

  // 考虑 null 情况,参数默认赋值会无效

  if (!ctx) ctx = window;

  // 将 this 函数保存在 ctx 上

  ctx.fn = this;

  // 传参执行并保存返回值

  const result = ctx.fn(...args);

  // 删除 ctx 上的 fn

  delete ctx.fn;

 

  return result;

};

// ------------------------------ 测试 ------------------------------

const numbers = [5, 6, 2, 3, 7];

// Function.prototype.__apply()

console.log('Function.prototype.__apply()');

const max = Math.max.__apply(null, numbers);

console.log(max); // 7

// Function.prototype.apply()

console.log('Function.prototype.apply()');

const min = Math.min.apply(null, numbers);

console.log(min); // 2



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。