手撕JS(可能持续更新···)

image

  关于实现js中一些常见的方法属于面试中的常问问题,可能刚开始接触的时候会一筹莫展。知道和理解其中的原理能够在日常开发中更如鱼得水,面对面试也不成问题。另外,学会以目的(实现的功能)为导向一层一层反推,总结出实现的思路就能按照步骤直接实现或者曲线实现(整理不易记得点赞哈)。

一、call的实现

  call()方法:让call()中的对象调用当前对象所拥有的function。例如:test.call(obj,arg1,arg2,···) 等价于 obj.test(arg1,arg2,···);在手写实现call()方法前我们先进行分析,test调用call方法可以看作将test方法作为obj的一个属性(方法)调用,等obj.test()执行完毕后,再从obj属性上删除test方法:

  • 1、将函数设置为对象的属性;
  • 2、处理传入的参数;
  • 3、执行对象上设置的函数;
  • 4、删除对象上第一步设置的函数;

myCall:

function test(a, b) {
  console.log(a);
  console.log(b);
  console.log(this.c);
}

let obj = {
  c: "hello",
};

//myCall
Function.prototype.myCall = function () {
  //声明传入上下文为传入的第一个参数,如果没有传参默认为global(node环境),如果是浏览器环境则为 window;
  let context = arguments[0] || global; 
  //将调用myCall方法函数(this)设置为 声明的传入上下文中的fn函数;
  context.fn = this;
  //对函数参数进行处理
  var args = [];
  for (let index = 0; index < arguments.length; index++) {
    index > 0 && args.push(arguments[index]);
  }
  //执行fn,也就是调用myCall方法的函数
  context.fn(...args);
    //执行完毕后删除传入上下文的fn,不改变原来的对象
  delete context.fn;
};

test.myCall(obj, "a", 123);
console.log(obj)

打印的结果:

a
123
hello
{ c: 'hello' }

从结果可以看出:testthis.c输出为hello,说明thisobj;最后输出的obj也没有改变。

二、apply的实现

  apply()方法作用和call()完全一样,只是apply的参数第一个为需要指向的对象,第二个参数以数组形式传入。例如:test.apply(obj,[arg1,arg2,···]) 等价于 obj.test(arg1,arg2,···)

myApply:

//myApply
Function.prototype.myApply = function(){
  let context = arguments[0] || global;
  context.fn = this;
  var args = arguments.length > 1 ? arguments[1] : [];
  context.fn(...args);
  delete context.fn;
}

test.myApply(obj, ["world", 123]);
console.log(obj)

打印的结果:

world
123
hello
{ c: 'hello' }

三、bind的实现

  bind方法:创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。例如:let fn = test.bind(obj,arg1,arg2,···); fn() 等价于 let fn = obj.test; fn(arg1,arg2,···);实现思路为:

  • 1、将函数设置为对象的属性;
  • 2、处理传入的参数;
  • 3、返回函数的定义/引用;
  • 4、外层执行接收的函数;
  • 5、删除对象上第一步设置的函数;

myBind:

Function.prototype.myBind = function(){
  //1.1、声明传入上下文为传入的第一个参数,如果没有传参默认为global(node环境),如果是浏览器环境则为 window;
  let context = arguments[0] || global;
  //1.2、将调用myBind方法函数(this)设置为 声明的传入上下文中的fn函数;
  context.fn = this;
  //2.1、对调用myBind的函数参数进行处理
  var args = [];
  for (let index = 0; index < arguments.length; index++) {
    index > 0 && args.push(arguments[index]);
  }
    //3、声明和定义函数变量F,用于返回给外层
  let F = function  (){
    //2.2、对再次传入的参数进行处理,追加到
    for (let index = 0; index < arguments.length; index++) {
      args.push(arguments[index]);
    }
    //4.2、执行实际的调用myBind方法函数
    context.fn(...args);
    //5、执行完毕后删除传入上下文的fn,不改变原来的对象
    delete context.fn;
  }
  return F;
}

var f = test.myBind(obj, "a")
//4.1、执行返回的函数
f(9527);

打印的结果:

a
9527
hello
{ c: 'hello' }

四、Promise的实现

1、分析Promise使用

  MDN中关Promise的定义如下:

Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。 一个 Promise 对象代表一个在这个 promise 被创建出来时不一定已知的值。它让您能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。 这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。
  一个 Promise 必然处于以下几种状态之一:
    待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
    已兑现(fulfilled): 意味着操作成功完成。
    已拒绝(rejected): 意味着操作失败。
  待定状态的 Promise 对象要么会通过一个值被兑现(fulfilled),要么会通过一个原因(错误)被拒绝(rejected)。当这些情况之一发生时,我们用 promise 的 then 方法排列起来的相关处理程序就会被调用。如果 promise 在一个相应的处理程序被绑定时就已经被兑现或被拒绝了,那么这个处理程序就会被调用,因此在完成异步操作和绑定处理方法之间不会存在竞争状态

new Promise((resolve, reject) => {
  //异步操作
  //···
  //执行完后调用resolve和reject输出两种不同结果
  if (true) {
    resolve("res");
  } else {
    reject("err");
  }
})
  .then((res) => { //then接受resolve中的结果
    console.log(res);
  })
  .catch((err) => { //catch接受reject中的结果
    console.log(err);
  });

Promise的使用分为三步:

  • 1、新建Promise实例,即通过new实现,同时接受一个函数参数,函数参数中接受resolve和reject两个形参(实质上也是函数);
  • 2、新建的Promise实例接受的函数参数中就是要执行的异步代码,并且用resolve和reject对异步结果进行调用输出;
  • 3、新建的Promise实例可以调用then和catch方法对异步结果进行接受和处理;

上述新建实例代码可以转化为:

function fn(resolve, reject) {
  //异步操作
  //···
  //执行完后调用resolve和reject输出两种不同结果
  if (true) {
    resolve("res");
  } else {
    reject("err");
  }
}

let p = new Promise(fn);

p.then((res) => { //then接受resolve中的结果
    console.log(res);
  })
  
p.catch((err) => { //catch接受reject中的结果
    console.log(err);
  });

  上述中的使用者就是then和catch,结合代码中的使用方式,简单来说就是Promise中执行异步操作,then和catch只会在异步执行完后才会接到返回结果继续执行!

2、手撕Promise

  了解了Promise的定义和使用步骤后,接下来直接手撕Promise的实现,直接上实现Promise的代码(内涵大量注释,基本一句一解释,但是逻辑还是得第三部分来讲):

// 定义promise中的三种状态
const STATUS_PENDING = "pending";
const STATUS_FULFILLED = "fulfilled";
const STATUS_REJECTED = "rejected";

// 定义promise的类
class myPromise {
  //class的构造函数,接受新建实例时的参数:executor在promise中是一个函数
  constructor(executor) {
    //初始化该class中的初始状态
    this.status = STATUS_PENDING;
    //定义class中成功(res)和失败(err)时的变量值
    this.res = "";
    this.err = "";

    //promis异步中最重要的异步,定义成功和错误函数存储的数组,存放异步时还没有执行的操作
    this.onResCallbacks = [];
    this.onErrCallbacks = [];

    //定义该构造函数constructor定义域中的变量resolve
    let resolve = (res) => {
      // 首先判断该class中的状态,只有状态为pending时才能转化class转态为fulfilled或者rejected
      if (this.status === STATUS_PENDING) {
        //修改class的转态为fulfilled,也就表示不会转进行其他转态的转化了
        this.status = STATUS_FULFILLED;
        //将成功(resolve)状态下的值赋给class的成功返回res
        this.res = res;
        //此时状态由pending转为fulfilled,执行之前在then中存放的需要执行的异步操作,promise的then中参数res接受结果
        this.onResCallbacks.forEach((fn) => {
          fn();
        });
      }
    };

    //定义该构造函数constructor定义域中的变量reject
    let reject = (err) => {
      // 首先判断该class中的状态,只有状态为pending时才能转化class转态为fulfilled或者rejected
      if (this.status === STATUS_PENDING) {
        //修改class的转态为rejected,也就表示不会转进行其他转态的转化了
        this.status = STATUS_REJECTED;
        //将失败(reject)状态下的值赋给class的失败返回err
        this.err = err;
        //此时状态由pending转为rejected,执行之前在catch中存放的需要执行的异步操作,promise的catch中参数err接受结果
        this.onErrCallbacks.forEach((fn) => {
          fn();
        });
      }
    };

    //按照promise中的逻辑,在调用时就立即执行了,所以在手写的myPromise创建构造函数constructor时就执行executor
    try {
      //执行参入的函数,并将上述定义的resolve和reject作为参数传入
      executor(resolve, reject);
    } catch (err) {
      //报错时调用失败的状态函数
      reject(err);
    }
  }
  
  //在class中定义promise的成功状态接收函数then,按照promise逻辑,then中传入的一般都是一个函数
  then(onRes = () => {}) {
    //如果是异步的,此时在constructor中status的状态还没变成fulfilled,所以会跳过onRes调用,没有返回
    if (this.status === STATUS_FULFILLED) {
      onRes(this.res);
    }
    //但是我们将此时的异步放入数组存放
    if (this.status === STATUS_PENDING) {
      this.onResCallbacks.push(() => onRes(this.res));
    }
    //这步操作保证了then和catch能够在同级一起"."调起,当then上述操作完后,返回class实例,便可以接在后面继续调用catch
    return this;
  }

  //在class中定义promise的失败状态接收函数catch,按照promise逻辑,catch中传入的一般都是一个函数
  catch(onErr = () => {}) {
    //如果是异步的,此时在constructor中status的状态还没变成rejected,所以会跳过onErr调用,没有返回
    if (this.status === STATUS_REJECTED) {
      onErr(this.err);
    }
    //但是我们将此时的异步放入数组存放
    if (this.status === STATUS_PENDING) {
      this.onErrCallbacks.push(() => onErr(this.err));
    }
     //这步操作保证了then和catch能够在同级一起"."调起,当catch上述操作完后,返回class实例,便可以接在后面继续调用then
    return this;
  }
}

//调用自己手写的promise
new myPromise((resolve, reject) => {
  console.log("进入了手写的promise");
  //用setTimeOut模拟异步操作
  setTimeout(() => {
    if (false) {
      resolve("输出成功结果resolve");
    } else {
      reject("输出失败结果reject");
    }
  }, 2000); //按照js的特性,此时不会等待异步完成,直接调用then或者catch
})
  .then((res) => {
    console.log("then:", res);
  })
  .catch((err) => { //return this具体作用体现在这里
    console.log("catch:", err);
  });
image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 方便起见不分parameter[形参,出现在函数定义中]和argument[实参,其值为传入函数的值],一律当作a...
    东月三二阅读 375评论 0 0
  • 闭包 变量作用域 变量根据作用域不同可以将函数分为全局变量和局部变量 函数内部可以使用全局变量 函数外部不可以使用...
    alfalfaw阅读 197评论 0 0
  • 1、es5和es6的区别,说一下你所知道的es6 ECMAScript5,即ES5,是ECMAScript的第五次...
    没糖_cristalle阅读 676评论 0 0
  • 夜莺2517阅读 127,711评论 1 9
  • 版本:ios 1.2.1 亮点: 1.app角标可以实时更新天气温度或选择空气质量,建议处女座就不要选了,不然老想...
    我就是沉沉阅读 6,876评论 1 6