最近面试比较多,经常被问到手写实现,不仅要写出来,还要懂为什么,说清楚。在这整理一下。
new
如何实现new?
- new Object()创建一个空对象- obj。
- 取得构造函数,也就是arguments的第一项。
- 将新对象的原型链接到传入的对象。新对象内部属性__proto__指向构造函数的原型prototype,新对象就可以访问构造函数原型中的属性和方法。
- 使用apply,将构造函数的this指向这个新对象,新对象就可以访问构造函数的属性和方法。
- 执行函数,取得返回值。如果返回值是一个对象,就返回该对象。否则返回obj。
function mynew() {
    let obj = new Object();
    let Con = [].shift.call(arguments);
    obj.__proto__ = Con.prototype;
    let res = Con.apply(obj, arguments)
    return typeof res == 'object' ? res : obj;
}
复制代码注意:
[].shift.call(arguments)是删除并拿到arguments的第一项。
当
[].shift.call()传入arguments对象时,call改变了shift方法原来的this指向,指向了arguments,所以shift删除并拿到arguments的第一项。同理,将类数组对象为数组也是如此。
[].slice.call(arguments)等效于Array.prototype.slice.call(arguments)。(
slice()用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组)。
call,apply,bind
call,apply,bind第一个参数都是函数上下文this。
共同点是,都能够改变函数执行时的上下文,将一个对象的方法交给另一个对象来执行,并且是立即执行的。
改变执行上下文就是说,A 对象有一个方法,而 B 对象也需要用到同样的方法,那么这时候我们让B借用一下 A 对象的方法,既完成了需求,又减少了内存的占用。
所以说,B想用A的方法,利用
function B( ) {
    A.call(this)
}
复制代码apply 和 call 的区别是 call 方法第二个参数接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组。
apply 和 call 是立即执行的,
bind 是创建一个新的函数,我们必须要手动去调用。
实现call
注意他们都是函数原型上的方法~
- 取传入的对象context。(this 参数可以传 null,当为 null 的时候,视为指向 window。)
- 将函数设为对象的一个属性方法。也就是把这个要调用call的函数方法设为context对象的一个属性。
- 处理传入的参数。
- 传入参数,执行该方法。
- 删除该方法。
- 返回结果。
Function.prototype.myCall = function (context) {
    var context = context || window;
    context.fn = this; 
    let args = [...arguments].slice(1);
    let result = context.fn(...args);
    delete context.fn;
    return result;
}
复制代码实现apply
apply的第二个参数传入的是一个数组,所以需要判断是否存在,存在需要将数组展开。
Function.prototype.defineApply = function (context, arr) {
    var context = context || window;
    context.fn = this;
    let result;
    // 需要判断是否存在第二个参数
    // 如果存在,就将第二个参数展开
    if (arguments[1]) {
      result = context.fn(...arguments[1]);
    } else {
      result = context.fn();
    }
    delete context.fn;
    return result;
}
复制代码实现bind
bind 也能改变对象的执行上下文,它与 call 和 apply 不同的是,返回值是一个函数,并且需要稍后再调用一下,才会执行。
//用call、apply模拟实现bind
Function.prototype.mybind = function (context) {
    let self = this; // 保存函数的引用
    return function () { // 返回一个新的函数
        // return self.apply(context, arguments);
        return self.call(context, arguments);
    }
};
复制代码Function.prototype.myBind = function (context) {
    if (typeof this !== 'function') {
      throw new TypeError('Error')
    }
    var _this = this
    var args = [...arguments].slice(1)
    // 返回一个函数
    return function F() {
      // 因为返回了一个函数,我们可以 new F(),所以需要判断
      if (this instanceof F) {
        return new _this(...args, ...arguments)
      }
      return _this.apply(context, args.concat(...arguments))
    }
}
复制代码防抖节流
防抖
你尽管触发事件,但我都在事件触发后的n秒执行,以最新触发事件为准。
思路:
维护一个timer,记录当前状态。
如果当前存在定时器,就删除他,因为以最新的触发事件为准。
重新设置定时器并执行。
这里用到了闭包,同来保存timer。如果不使用闭包的话,每次return里面都重新设置了新的timer,不能找到上一次的删除。
应用场景:搜索联想、窗口resize、登录时不断点击
function debounce(fn,wait) {
    let timer = null;
    return () => {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, arguments);
        }, wait);
    }
}
复制代码节流
持续触发事件,一段时间内只执行一次。
思路:时间戳,先将初始值设为0。
如果当前时间减去之前的时间戳大于设置的等待时间,则执行函数。更新当前时间戳。
应用场景:滚动事件scroll、鼠标不断点击触发
function throttle(func,wait) {
    let pre = 0;
    return () => {
        let cur = Date.now();
        // 如果两次时间间隔超过了指定时间,则执行函数。
        if (cur - pre > wait) {
            func.apply(this, arguments);
            pre = Date.now();
        }
    }
}
复制代码深拷贝和浅拷贝
浅拷贝
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址。
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址。
浅拷贝的方式:
- Object.assign
- Array.prototype.slice()
- Array.prototype.concat()
- 扩展运算符
function slowclone(obj) {
    let cloneobj = {};
    for (let k in obj) {
        if (obj.hasOwnProperty(k)) {
            cloneobj[k]=obj[k]
        }
    }
    return cloneobj;
}
复制代码注意:for in遍历,会遍历原型链里面的属性,所以使用hasOwnProperty排除原型链。
深拷贝
深拷贝开辟一个新的栈,两个对象属性完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
递归实现深拷贝:
遍历数组、对象,直到里面都是基本数据类型,然后再去复制。
- 如果不是对象或者是null,直接返回,不需要拷贝。
- 如果是Date或者正则,返回new的新实例。
需要注意:对象存在循环引用的问题,对象的属性引用了对象自身。所以可以额外开辟一个存储空间,存储当前对象和存储对象之间的关系。需要拷贝当前对象时,先去这个存储空间里找,有没有拷贝过这个对象,如果有,直接返回存储空间里的对象,没有的话继续拷贝。
function deepclone(obj, hash = new Map()) {
  if (!obj || typeof obj !== "object") return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  let cloneobj = obj.constructor();
  if (hash.get(obj)) return hash.get(obj);
  hash.set(obj, cloneobj);
  for (let k in obj) {
    if (obj.hasOwnProperty(k)) {
      cloneobj[k] = deepclone(obj[k], hash);
    }
  }
  return cloneobj;
}
复制代码instanceof
判断B是不是在A的原型链上。
不断地去找A的__proto__直到为B.prototype或者null。
function myinstanceof(A, B) {
    let protoA = A.__proto__;
    let prototypeB = B.prototype;
    while (true) {
        if (protoA == null) {
            return false;
        }
        if (protoA == prototypeB) {
            return true;
        }
        protoA = protoA.__proto__;
    }
}
复制代码Object.create()
用于创建一个新对象,被创建的对象继承另一个对象(o)的原型。所以F.prototype = o;
function createObj(o) {
  //传入的参数o为返回实例的__porto__,也就是实例构造函数的显式原型
  function F() {} //构造函数
  F.prototype = o;
  return new F(); //返回实例
}
复制代码map
Map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后 append 到新的数组中。
Array.prototype.newMap = function (fn) {
  let newArr = [];
  for (let i = 0; i < this.length; i++) {
    newArr.push(fn(this[i], i, this)); //this指向调用newMap方法的数组
  }
  return newArr;
};
let arr = [1, 2, 3];
let res = arr.newMap((a) => a + 1);
console.log(res);
复制代码reduce的参数如下
arr.reduce((previousValue, currentValue, currentIndex, array) => {}, initialValue)
    
复制代码reduce实现map
Array.prototype.newMap = function (fn, Arg) {
  var res = [];
  this.reduce((prev, curr, index, array) => {
    res.push(fn.call(Arg, curr, index, array));
  }, 0); //指定初始值initialValue=0,所以从currentIndex=0开始,即第一个开始  
  return res;
};
复制代码forEach
forEach()方法对数组的每个元素执行一次给定的函数。
arr.forEach(function(currentValue, currentIndex, arr) {}, thisArg)
//currentValue  必需。当前元素
//currentIndex  可选。当前元素的索引
//arr           可选。当前元素所属的数组对象。
//thisArg       可选参数。当执行回调函数时,用作 this 的值。
复制代码Array.prototype._forEach = function(fn, thisArg) {
    if (typeof fn !== 'function') throw "参数必须为函数";
    if(!Array.isArray(this)) throw "只能对数组使用forEach方法";
    let arr = this;
    for(let i=0; i<arr.length; i++) {
        fn.call(thisArg, arr[i], i, arr)
    }
}
复制代码Promise
• Promise 就是一个对象,用来表示并传递异步操作的最终结果。
解决回调函数层层嵌套产生的回调地狱问题。
Promise
function myPromise(executor) {
  let self = this; //保留this。防止后面方法出现this指向不明的问题
  self.status = 'pending';//promise的默认状态是pending
  self.value = undefined;//保存成功回调传递的值
  self.reason = undefined;//保存失败回调传递的值
  self.successCB = [];//存储fulfilled状态对应的回调函数
  self.failCB = [];//存储rejected状态对应的回调函数
  
  function resolve(value) {
    if (self.status === 'pending') { // 只能由pending状态 => fulfilled状态 (避免调用多次resolve reject)
      self.status = 'resolved';//成功函数将其状态修改为resolved
      self.value = value;//将成功的值保存起来
      self.successCB.forEach(fn=>fn());
    }
  }
  function reject(reason) {
    if (self.status === 'pending') { // 只能由pending状态 => rejected状态 
      self.status = 'rejected';//失败函数将其函数修改为rejected
      self.reason = reason;//将失败的值保存起来
      self.failCB.forEach(fn=>fn());
    }
  }
 // 捕获在excutor执行器中抛出的异常
  try {
    executor(resolve,reject)
  } catch (err) {
    reject(err)
  }
}
复制代码Promise.prototype.then
then方法是原型链上的方法
myPromise.prototype.then = function (onResolved, onRejected) {
  let self = this;
  if (self.status === 'pending') {
    self.successCB.push(() => {
      onResolved(self.value);//将resolve函数保留的成功值传递作为参数
    })
    self.failCB.push(() => {
      onRejected(self.reason);//将reject函数保留的失败值传递作为参数
    })
  }
  if (self.status === 'resolved') {
    onResolved(self.value);//将resolve函数保留的成功值传递作为参数
  }
  if (self.status === 'rejected') {
    onRejected(self.reason);//将reject函数保留的失败值传递作为参数
  }
}
复制代码Promise.all
Promise.all 接收一个 promise 对象的数组作为参数,当这个数组里的所有 promise 对象全部变为resolve或者有一个reject状态出现的时候,它才会去调用.then方法,它们是并发执行的。
Promise.all()方法将多个Promise实例包装成一个Promise对象(p),接受一个数组(p1,p2,p3)作为参数,数组中不一定需要都是Promise对象,但是一定具有Iterator接口,如果不是的话,就会调用Promise.resolve将其转化为Promise对象之后再进行处理。
使用Promise.all()生成的Promise对象(p)的状态是由数组中的Promise对象(p1,p2,p3)决定的;
1、如果所有的Promise对象(p1,p2,p3)都变成fullfilled状态的话,生成的Promise对象(p)也会变成fullfilled状态,p1,p2,p3三个Promise对象产生的结果会组成一个数组返回给传递给p的回调函数;
2、如果p1,p2,p3中有一个Promise对象变为rejected状态的话,p也会变成rejected状态,第一个被rejected的对象的返回值会传递给p的回调函数。
function myall(promises) {
    return new Promise((resolve, reject) => {//返回一个新的Promise
        let ret = [];//定义一个空数组存放结果
        let count = 0;
        let done = (i, data) => {//处理数据函数
            ret[i] = data;
            count++;
            if (count === promises.length) {//当i等于传递的数组的长度时 
                resolve(ret); //执行resolve,并将结果放入
            }
        }
        for (let i = 0; i < promises.length; i++){
            promises[i].then((data) => done(i, data), reject); //将结果和索引传入done函数
        }
    })
}
复制代码Promise.race
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
promise.race()中,promise的状态只能改变一次,即resolve和reject都只被能执行一次。
function myrace(promises) {
  return new Promise(function (resolve, reject) {
    for (let i = 0; i < promises.length; i++) {
      promises[i].then(resolve, reject);
    }
  });
}
复制代码柯里化
给函数分步传参,每次传递部分参数,并返回一个更具体的函数来接受剩余的参数。这中间可能接受多层这样的接受部分参数的函数,直至返回结果。
function curry(fn, args) {
    var length = fn.length;//获取fn形参的个数
    var args = args || [];//获取上一次的参数
    return function(){//返回一个函数
        //获取本次的参数并转换为数组
        var newArgs = Array.prototype.slice.call(arguments);
        newArgs=args.concat(newArgs);//将本次参数与上次的参数合并
        if (newArgs.length < length) {//如果参数个数小于形参个数,继续收集
            return curry.call(this,fn,newArgs);
        }else{
            return fn.apply(this,newArgs);//否则返回函数执行结果
        }
    }
}
复制代码柯里化求和也很重要!
Object.prototype.toString() 方法返回一个表示该对象的字符串,当对象被表示为文本值时或者当以期望字符串的方式引用对象时,该方法被自动调用。
简单理解:在某个操作或者运算需要字符串而该对象又不是字符串的时候,会触发该对象的 String 转换,会将非字符串的类型尝试自动转为 String 类型。
function curyAdd(){
    var args = Array.prototype.slice.call(arguments);
    var adder = function () {
        args.push(...arguments);
        return adder;
    }
    adder.toString = function () {
        return args.reduce((pre, cur) => {
            return pre+cur;
        },0)
    }
    return adder;
}
console.log(cury(1, 2)(3));
复制代码Ajax
Ajax的原理简单来说是在用户和服务器之间加了—个中间层(AJAX引擎),通过XmlHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用javascript来操作DOM而更新页面。使用户操作与服务器响应异步化。这其中最关键的一步就是从服务器获得请求数据。
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
  if (xhr.readyState == 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
      console.log(xhr.responseText);
    } else {
      console.error(xhr.statusText);
    }
  }
}
// 请求的类型、请求的 url 以及是否异步发送请求
xhr.open("get", url, true);
// 传入请求的数据
xhr.send(null);
复制代码俺是分割线~~~~
后续遇到其他的再补充!























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)
