<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試官問:能否模擬實現(xiàn)JS的bind方法(高頻考點)

          共 20470字,需瀏覽 41分鐘

           ·

          2021-07-16 18:33

          寫于2018年11月21日,發(fā)布在掘金閱讀量1.3w+

          前言

          這是面試官問系列的第二篇,旨在幫助讀者提升JS基礎(chǔ)知識,包含new、call、apply、this、繼承相關(guān)知識。
          面試官問系列文章如下:感興趣的讀者可以點擊閱讀。
          1.面試官問:能否模擬實現(xiàn)JS的new操作符
          2.面試官問:能否模擬實現(xiàn)JS的bind方法(本文)
          3.面試官問:能否模擬實現(xiàn)JS的call和apply方法
          4.面試官問:JS的this指向
          5.面試官問:JS的繼承

          用過React的同學(xué)都知道,經(jīng)常會使用bind來綁定this。

          import React, { Component } from 'react';
          class TodoItem extends Component{
              constructor(props){
                  super(props);
                  this.handleClick = this.handleClick.bind(this);
              }
              handleClick(){
                  console.log('handleClick');
              }
              render(){
                  return  (
                      <div onClick={this.handleClick}>點擊</div>
                  );
              };
          }
          export default TodoItem;

          那么面試官可能會問是否想過bind到底做了什么,怎么模擬實現(xiàn)呢。

          附上之前寫文章寫過的一段話:已經(jīng)有很多模擬實現(xiàn)bind的文章,為什么自己還要寫一遍呢。學(xué)習(xí)就好比是座大山,人們沿著不同的路登山,分享著自己看到的風(fēng)景。你不一定能看到別人看到的風(fēng)景,體會到別人的心情。只有自己去登山,才能看到不一樣的風(fēng)景,體會才更加深刻。

          先看一下bind是什么。從上面的React代碼中,可以看出bind執(zhí)行后是函數(shù),并且每個函數(shù)都可以執(zhí)行調(diào)用它。眼見為實,耳聽為虛。讀者可以在控制臺一步步點開例子1中的obj:

          var obj = {};
          console.log(obj);
          console.log(typeof Function.prototype.bind); // function
          console.log(typeof Function.prototype.bind());  // function
          console.log(Function.prototype.bind.name);  // bind
          console.log(Function.prototype.bind().name);  // bound
          Function.prototype.bind

          因此可以得出結(jié)論1:

          1、bindFunctoin原型鏈中Function.prototype的一個屬性,每個函數(shù)都可以調(diào)用它。
          2、bind本身是一個函數(shù)名為bind的函數(shù),返回值也是函數(shù),函數(shù)名是bound。(打出來就是bound加上一個空格)。知道了bind是函數(shù),就可以傳參,而且返回值'bound '也是函數(shù),也可以傳參,就很容易寫出例子2
          后文統(tǒng)一 bound 指原函數(shù)original bind之后返回的函數(shù),便于說明。

          var obj = {
              name'若川',
          };
          function original(a, b){
              console.log(this.name);
              console.log([a, b]);
              return false;
          }
          var bound = original.bind(obj, 1);
          var boundResult = bound(2); // '若川', [1, 2]
          console.log(boundResult); // false
          console.log(original.bind.name); // 'bind'
          console.log(original.bind.length); // 1
          console.log(original.bind().length); // 2 返回original函數(shù)的形參個數(shù)
          console.log(bound.name); // 'bound original'
          console.log((function(){}).bind().name); // 'bound '
          console.log((function(){}).bind().length); // 0

          由此可以得出結(jié)論2:

          1、調(diào)用bind的函數(shù)中的this指向bind()函數(shù)的第一個參數(shù)。

          2、傳給bind()的其他參數(shù)接收處理了,bind()之后返回的函數(shù)的參數(shù)也接收處理了,也就是說合并處理了。

          3、并且bind()后的namebound + 空格 + 調(diào)用bind的函數(shù)名。如果是匿名函數(shù)則是bound + 空格。

          4、bind后的返回值函數(shù),執(zhí)行后返回值是原函數(shù)(original)的返回值。

          5、bind函數(shù)形參(即函數(shù)的length)是1。bind后返回的bound函數(shù)形參不定,根據(jù)綁定的函數(shù)原函數(shù)(original)形參個數(shù)確定。

          根據(jù)結(jié)論2:我們就可以簡單模擬實現(xiàn)一個簡版bindFn

          // 第一版 修改this指向,合并參數(shù)
          Function.prototype.bindFn = function bind(thisArg){
              if(typeof this !== 'function'){
                  throw new TypeError(this + 'must be a function');
              }
              // 存儲函數(shù)本身
              var self = this;
              // 去除thisArg的其他參數(shù) 轉(zhuǎn)成數(shù)組
              var args = [].slice.call(arguments1);
              var bound = function(){
                  // bind返回的函數(shù) 的參數(shù)轉(zhuǎn)成數(shù)組
                  var boundArgs = [].slice.call(arguments);
                  // apply修改this指向,把兩個函數(shù)的參數(shù)合并傳給self函數(shù),并執(zhí)行self函數(shù),返回執(zhí)行結(jié)果
                  return self.apply(thisArg, args.concat(boundArgs));
              }
              return bound;
          }
          // 測試
          var obj = {
              name'若川',
          };
          function original(a, b){
              console.log(this.name);
              console.log([a, b]);
          }
          var bound = original.bindFn(obj, 1);
          bound(2); // '若川', [1, 2]

          如果面試官看到你答到這里,估計對你的印象60、70分應(yīng)該是會有的。但我們知道函數(shù)是可以用new來實例化的。那么bind()返回值函數(shù)會是什么表現(xiàn)呢。
          接下來看例子3

          var obj = {
              name'若川',
          };
          function original(a, b){
              console.log('this'this); // original {}
              console.log('typeof this'typeof this); // object
              this.name = b;
              console.log('name'this.name); // 2
              console.log('this'this);  // original {name: 2}
              console.log([a, b]); // 1, 2
          }
          var bound = original.bind(obj, 1);
          var newBoundResult = new bound(2);
          console.log(newBoundResult, 'newBoundResult'); // original {name: 2}

          例子3種可以看出this指向了new bound()生成的新對象。

          可以分析得出結(jié)論3:

          1、bind原先指向obj的失效了,其他參數(shù)有效。

          2、new bound的返回值是以original原函數(shù)構(gòu)造器生成的新對象。original原函數(shù)的this指向的就是這個新對象。另外前不久寫過一篇文章:面試官問:能否模擬實現(xiàn)JS的new操作符。簡單摘要:new做了什么:

          1.創(chuàng)建了一個全新的對象。
          2.這個對象會被執(zhí)行[[Prototype]](也就是__proto__)鏈接。
          3.生成的新對象會綁定到函數(shù)調(diào)用的this。
          4.通過new創(chuàng)建的每個對象將最終被[[Prototype]]鏈接到這個函數(shù)的prototype對象上。
          5.如果函數(shù)沒有返回對象類型Object(包含Functoin, Array, Date, RegExg, Error),那么new表達式中的函數(shù)調(diào)用會自動返回這個新的對象。

          所以相當(dāng)于new調(diào)用時,bind的返回值函數(shù)bound內(nèi)部要模擬實現(xiàn)new實現(xiàn)的操作。話不多說,直接上代碼。

          // 第三版 實現(xiàn)new調(diào)用
          Function.prototype.bindFn = function bind(thisArg){
              if(typeof this !== 'function'){
                  throw new TypeError(this + ' must be a function');
              }
              // 存儲調(diào)用bind的函數(shù)本身
              var self = this;
              // 去除thisArg的其他參數(shù) 轉(zhuǎn)成數(shù)組
              var args = [].slice.call(arguments1);
              var bound = function(){
                  // bind返回的函數(shù) 的參數(shù)轉(zhuǎn)成數(shù)組
                  var boundArgs = [].slice.call(arguments);
                  var finalArgs = args.concat(boundArgs);
                  // new 調(diào)用時,其實this instanceof bound判斷也不是很準確。es6 new.target就是解決這一問題的。
                  if(this instanceof bound){
                      // 這里是實現(xiàn)上文描述的 new 的第 1, 2, 4 步
                      // 1.創(chuàng)建一個全新的對象
                      // 2.并且執(zhí)行[[Prototype]]鏈接
                      // 4.通過`new`創(chuàng)建的每個對象將最終被`[[Prototype]]`鏈接到這個函數(shù)的`prototype`對象上。
                      // self可能是ES6的箭頭函數(shù),沒有prototype,所以就沒必要再指向做prototype操作。
                      if(self.prototype){
                          // ES5 提供的方案 Object.create()
                          // bound.prototype = Object.create(self.prototype);
                          // 但 既然是模擬ES5的bind,那瀏覽器也基本沒有實現(xiàn)Object.create()
                          // 所以采用 MDN ployfill方案 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create
                          function Empty(){}
                          Empty.prototype = self.prototype;
                          bound.prototype = new Empty();
                      }
                      // 這里是實現(xiàn)上文描述的 new 的第 3 步
                      // 3.生成的新對象會綁定到函數(shù)調(diào)用的`this`。
                      var result = self.apply(this, finalArgs);
                      // 這里是實現(xiàn)上文描述的 new 的第 5 步
                      // 5.如果函數(shù)沒有返回對象類型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),
                      // 那么`new`表達式中的函數(shù)調(diào)用會自動返回這個新的對象。
                      var isObject = typeof result === 'object' && result !== null;
                      var isFunction = typeof result === 'function';
                      if(isObject || isFunction){
                          return result;
                      }
                      return this;
                  }
                  else{
                      // apply修改this指向,把兩個函數(shù)的參數(shù)合并傳給self函數(shù),并執(zhí)行self函數(shù),返回執(zhí)行結(jié)果
                      return self.apply(thisArg, finalArgs);
                  }
              };
              return bound;
          }

          面試官看到這樣的實現(xiàn)代碼,基本就是滿分了,心里獨白:這小伙子/小姑娘不錯啊。不過可能還會問this instanceof bound不準確問題。上文注釋中提到this instanceof bound也不是很準確,ES6 new.target很好的解決這一問題,我們舉個例子4:

          instanceof 不準確,ES6 new.target很好的解決這一問題

          function Student(name){
              if(this instanceof Student){
                  this.name = name;
                  console.log('name', name);
              }
              else{
                  throw new Error('必須通過new關(guān)鍵字來調(diào)用Student。');
              }
          }
          var student = new Student('若');
          var notAStudent = Student.call(student, '川'); // 不拋出錯誤,且執(zhí)行了。
          console.log(student, 'student', notAStudent, 'notAStudent');

          function Student2(name){
              if(typeof new.target !== 'undefined'){
                  this.name = name;
                  console.log('name', name);
              }
              else{
                  throw new Error('必須通過new關(guān)鍵字來調(diào)用Student2。');
              }
          }
          var student2 = new Student2('若');
          var notAStudent2 = Student2.call(student2, '川');
          console.log(student2, 'student2', notAStudent2, 'notAStudent2'); // 拋出錯誤

          細心的同學(xué)可能會發(fā)現(xiàn)了這版本的代碼沒有實現(xiàn)bind后的bound函數(shù)的nameMDN Function.name和lengthMDN Function.length。面試官可能也發(fā)現(xiàn)了這一點繼續(xù)追問,如何實現(xiàn),或者問是否看過es5-shim的源碼實現(xiàn)L201-L335。如果不限ES版本。其實可以用ES5Object.defineProperties來實現(xiàn)。

          Object.defineProperties(bound, {
              'length': {
                  value: self.length,
              },
              'name': {
                  value'bound ' + self.name,
              }
          });

          es5-shim的源碼實現(xiàn)bind

          直接附上源碼(有刪減注釋和部分修改等)

          var $Array = Array;
          var ArrayPrototype = $Array.prototype;
          var $Object = Object;
          var array_push = ArrayPrototype.push;
          var array_slice = ArrayPrototype.slice;
          var array_join = ArrayPrototype.join;
          var array_concat = ArrayPrototype.concat;
          var $Function = Function;
          var FunctionPrototype = $Function.prototype;
          var apply = FunctionPrototype.apply;
          var max = Math.max;
          // 簡版 源碼更復(fù)雜些。
          var isCallable = function isCallable(value){
              if(typeof value !== 'function'){
                  return false;
              }
              return true;
          };
          var Empty = function Empty({};
          // 源碼是 defineProperties
          // 源碼是bind筆者改成bindFn便于測試
          FunctionPrototype.bindFn = function bind(that{
              var target = this;
              if (!isCallable(target)) {
                  throw new TypeError('Function.prototype.bind called on incompatible ' + target);
              }
              var args = array_slice.call(arguments1);
              var bound;
              var binder = function ({
                  if (this instanceof bound) {
                      var result = apply.call(
                          target,
                          this,
                          array_concat.call(args, array_slice.call(arguments))
                      );
                      if ($Object(result) === result) {
                          return result;
                      }
                      return this;
                  } else {
                      return apply.call(
                          target,
                          that,
                          array_concat.call(args, array_slice.call(arguments))
                      );
                  }
              };
              var boundLength = max(0, target.length - args.length);
              var boundArgs = [];
              for (var i = 0; i < boundLength; i++) {
                  array_push.call(boundArgs, '$' + i);
              }
              // 這里是Function構(gòu)造方式生成形參length $1, $2, $3...
              bound = $Function('binder''return function (' + array_join.call(boundArgs, ',') + '){ return binder.apply(this, arguments); }')(binder);

              if (target.prototype) {
                  Empty.prototype = target.prototype;
                  bound.prototype = new Empty();
                  Empty.prototype = null;
              }
              return bound;
          };

          你說出es5-shim源碼bind實現(xiàn),感慨這代碼真是高效、嚴謹。面試官心里獨白可能是:你就是我要找的人,薪酬福利你可以和HR去談下。

          最后總結(jié)一下

          1、bindFunction原型鏈中的Function.prototype的一個屬性,它是一個函數(shù),修改this指向,合并參數(shù)傳遞給原函數(shù),返回值是一個新的函數(shù)。
          2、bind返回的函數(shù)可以通過new調(diào)用,這時提供的this的參數(shù)被忽略,指向了new生成的全新對象。內(nèi)部模擬實現(xiàn)了new操作符。
          3、es5-shim源碼模擬實現(xiàn)bind時用Function實現(xiàn)了length
          事實上,平時其實很少需要使用自己實現(xiàn)的投入到生成環(huán)境中。但面試官通過這個面試題能考察很多知識。比如this指向,原型鏈,閉包,函數(shù)等知識,可以擴展很多。
          讀者發(fā)現(xiàn)有不妥或可改善之處,歡迎指出。另外覺得寫得不錯,可以點個贊,也是對筆者的一種支持。

          文章中的例子和測試代碼放在github中bind模擬實現(xiàn) github。bind模擬實現(xiàn) 預(yù)覽地址 F12看控制臺輸出,結(jié)合source面板查看效果更佳。

          // 最終版 刪除注釋 詳細注釋版請看上文
          Function.prototype.bind = Function.prototype.bind || function bind(thisArg){
              if(typeof this !== 'function'){
                  throw new TypeError(this + ' must be a function');
              }
              var self = this;
              var args = [].slice.call(arguments1);
              var bound = function(){
                  var boundArgs = [].slice.call(arguments);
                  var finalArgs = args.concat(boundArgs);
                  if(this instanceof bound){
                      if(self.prototype){
                          function Empty(){}
                          Empty.prototype = self.prototype;
                          bound.prototype = new Empty();
                      }
                      var result = self.apply(this, finalArgs);
                      var isObject = typeof result === 'object' && result !== null;
                      var isFunction = typeof result === 'function';
                      if(isObject || isFunction){
                          return result;
                      }
                      return this;
                  }
                  else{
                      return self.apply(thisArg, finalArgs);
                  }
              };
              return bound;
          }

          推薦閱讀

          我在阿里招前端,我該怎么幫你?(現(xiàn)在還可以加模擬面試群)
          如何拿下阿里巴巴 P6 的前端 Offer
          如何準備阿里P6/P7前端面試--項目經(jīng)歷準備篇
          大廠面試官常問的亮點,該如何做出?
          如何從初級到專家(P4-P7)打破成長瓶頸和有效突破
          若川知乎問答:2年前端經(jīng)驗,做的項目沒什么技術(shù)含量,怎么辦?
          若川知乎高贊:有哪些必看的 JS庫?

          末尾

          你好,我是若川,江湖人稱菜如若川,歷時一年只寫了一個學(xué)習(xí)源碼整體架構(gòu)系列~(點擊藍字了解我)

          1. 關(guān)注若川視野,回復(fù)"pdf" 領(lǐng)取優(yōu)質(zhì)前端書籍pdf,回復(fù)"1",可加群長期交流學(xué)習(xí)
          2. 我的博客地址:https://lxchuan12.gitee.io 歡迎收藏
          3. 覺得文章不錯,可以點個在看呀^_^另外歡迎留言交流~
          精選前端好文,伴你不斷成長
          若川原創(chuàng)文章精選!可點擊
          瀏覽 65
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  xxx.一区 | 人人摸人人搞人人操 | 俺去官网一区 | 日韩一级电影观看 | 亚州第一成人网站 |