常见的手写实现

最近面试比较多,经常被问到手写实现,不仅要写出来,还要懂为什么,说清楚。在这整理一下。

new

如何实现new?

  1. new Object()创建一个空对象obj
  2. 取得构造函数,也就是arguments的第一项。
  3. 将新对象的原型链接到传入的对象。新对象内部属性__proto__指向构造函数的原型prototype,新对象就可以访问构造函数原型中的属性和方法。
  4. 使用apply,将构造函数的this指向这个新对象,新对象就可以访问构造函数的属性和方法。
  5. 执行函数,取得返回值。如果返回值是一个对象,就返回该对象。否则返回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)
}
复制代码

applycall 的区别是 call 方法第二个参数接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组。
applycall 是立即执行的,
bind 是创建一个新的函数,我们必须要手动去调用。

实现call

注意他们都是函数原型上的方法~

  1. 取传入的对象context。(this 参数可以传 null,当为 null 的时候,视为指向 window。)
  2. 将函数设为对象的一个属性方法。也就是把这个要调用call的函数方法设为context对象的一个属性。
  3. 处理传入的参数。
  4. 传入参数,执行该方法。
  5. 删除该方法。
  6. 返回结果。
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();
        }
    }
}
复制代码

深拷贝和浅拷贝

浅拷贝

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址。

浅拷贝的方式:

  1. Object.assign
  2. Array.prototype.slice()
  3. Array.prototype.concat()
  4. 扩展运算符
function slowclone(obj) {
    let cloneobj = {};
    for (let k in obj) {
        if (obj.hasOwnProperty(k)) {
            cloneobj[k]=obj[k]
        }
    }
    return cloneobj;
}
复制代码

注意:for in遍历,会遍历原型链里面的属性,所以使用hasOwnProperty排除原型链。

深拷贝

深拷贝开辟一个新的栈,两个对象属性完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。

递归实现深拷贝:
遍历数组、对象,直到里面都是基本数据类型,然后再去复制。

  1. 如果不是对象或者是null,直接返回,不需要拷贝。
  2. 如果是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);
复制代码

俺是分割线~~~~

后续遇到其他的再补充!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享