<mark id="cjdas"></mark>

    <ruby id="cjdas"></ruby>
    <tbody id="cjdas"><delect id="cjdas"><wbr id="cjdas"></wbr></delect></tbody>
      <tbody id="cjdas"><acronym id="cjdas"></acronym></tbody>
    <listing id="cjdas"></listing>

    作家
    登錄

    一道面試題引發的對JavaScript類型轉換的思考

    作者: 來源: 2017-03-10 13:03:14 閱讀 我要評論

       最近群里有人發了下面這題:

      實現一個函數,運算結果可以滿足如下預期結果:

      add(1)(2) // 3

      add(1, 2, 3)(10) // 16

      add(1)(2)(3)(4)(5) // 15

      對于一個好奇的切圖仔來說,忍不住動手嘗試了一下,看到題目首先想到的是會用到高階函數以及 Array.prototype.reduce()。

      高階函數(Higher-order function):高階函數的意思是它接收另一個函數作為參數。在 javascript 中,函數是一等公民,允許函數作為參數或者返回值傳遞。

      得到了下面這個解法:

      function add() {

      var args = Array.prototype.slice.call(arguments);

      return function() {

      var arg2 = Array.prototype.slice.call(arguments);

      return args.concat(arg2).reduce(function(a, b){

      return a + b;

      });

      }

      }

      驗證了一下,發現錯了:

      add(1)(2) // 3

      add(1, 2)(3) // 6

      add(1)(2)(3) // Uncaught TypeError: add(...)(...) is not a function(…)

      上面的解法,只有在 add()() 情形下是正確的。而當鏈式操作的參數多于兩個或者少于兩個的時候,無法返回結果。

      而這個也是這題的一個難點所在,add()的時候,如何既返回一個值又返回一個函數以供后續繼續調用?

      后來經過高人指點,通過重寫函數的 valueOf 方法或者 toString 方法,可以得到其中一種解法:

      function add () {

      var args = Array.prototype.slice.call(arguments);

      var fn = function () {

      var arg_fn = Array.prototype.slice.call(arguments);

      return add.apply(null, args.concat(arg_fn));

      }

      fn.valueOf = function () {

      return args.reduce(function(a, b) {

      return a + b;

      })

      }

      return fn;

      }

      嗯?第一眼看到這個解法的時候,我是懵逼的。因為我感覺 fn.valueOf() 從頭到尾都沒有被調用過,但是驗證了下結果:

      add(1) // 1

      add(1,2)(3) //6

      add(1)(2)(3)(4)(5) // 15

      神奇的對了!那么玄機必然是在上面的 fn.valueOf = function() {} 內了。為何會是這樣呢?這個方法是在函數的什么時刻執行的?且聽我一步一步道來。

      valueOf 和 toString

      先來簡單了解下這兩個方法:

      Object.prototype.valueOf()

      用 MDN 的話來說,valueOf() 方法返回指定對象的原始值。

      JavaScript 調用 valueOf() 方法用來把對象轉換成原始類型的值(數值、字符串和布爾值)。但是我們很少需要自己調用此函數,valueOf 方法一般都會被 JavaScript 自動調用。

      記住上面這句話,下面我們會細說所謂的自動調用是什么意思。

      Object.prototype.toString()

      toString() 方法返回一個表示該對象的字符串。

      每個對象都有一個 toString() 方法,當對象被表示為文本值時或者當以期望字符串的方式引用對象時,該方法被自動調用。

      這里先記住,valueOf() 和 toString() 在特定的場合下會自行調用。

      原始類型

      好,鋪墊一下,先了解下 javascript 的幾種原始類型,除去 Object 和 Symbol,有如下幾種原始類型:

      Number

      String

      Boolean

      Undefined

      Null

      在 JavaScript 進行對比或者各種運算的時候會把對象轉換成這些類型,從而進行后續的操作,下面逐一說明:

      String 類型轉換

      在某個操作或者運算需要字符串而該對象又不是字符串的時候,會觸發該對象的 String 轉換,會將非字符串的類型嘗試自動轉為 String 類型。系統內部會自動調用 toString 函數。舉個例子:

      var obj = {name: 'Coco'};

      var str = '123' + obj;

      console.log(str); // 123[object Object]

      轉換規則:

      如果 toString 方法存在并且返回原始類型,返回 toString 的結果。

      如果 toString 方法不存在或者返回的不是原始類型,調用 valueOf 方法,如果 valueOf 方法存在,并且返回原始類型數據,返回 valueOf 的結果。

      其他情況,拋出錯誤。

      上面的例子實際上是:

      var obj = {name: 'Coco'};

      var str = '123' + obj.toString();

      其中,obj.toString() 的值為 "[object Object]"。

      假設是數組:

      var arr = [1, 2];

      var str = '123' + arr;

      console.log(str); // 1231,2

      上面 + arr ,由于這里是個字符串加操作,后面的 arr 需要轉化為一個字符串類型,所以其實是調用了 + arr.toString() 。

      但是,我們可以自己改寫對象的 toString,valueOf 方法:

      var obj = {

      toString: function() {

      console.log('調用了 obj.toString');

      return {};

      },

      valueOf: function() {

      console.log('調用了 obj.valueOf')

      return '110';

      }

      }

      alert(obj);

      // 調用了 obj.toString

      // 調用了 obj.valueOf

      // 110

      上面 alert(obj + '1') ,obj 會自動調用自己的 obj.toString() 方法轉化為原始類型,如果我們不重寫它的 toString 方法,將輸出 [object Object]1 ,這里我們重寫了 toString ,而且返回了一個原始類型字符串 111 ,所以最終 alert 出了 1111。

      上面的轉化規則寫了,toString 方法需要存在并且返回原始類型,那么如果返回的不是一個原始類型,則會去繼續尋找對象的 valueOf 方法:

      下面我們嘗試證明如果在一個對象嘗試轉換為字符串的過程中,如果 toString() 方法不可用的時候,會發生什么。

      這個時候系統會再去調用 valueOf() 方法,下面我們改寫對象的 toString 和 valueOf:

      var obj = {

      toString: function() {

      console.log('調用了 obj.toString');

      return {};

      },

      valueOf: function() {

      console.log('調用了 obj.valueOf')

      return '110';

      }

      }

      alert(obj);

      // 調用了 obj.toString

      // 調用了 obj.valueOf

      // 110

      從結果可以看到,當 toString 不可用的時候,系統會再嘗試 valueOf 方法,如果 valueOf 方法存在,并且返回原始類型(String、Number、Boolean)數據,返回valueOf的結果。

      那么如果,toString 和 valueOf 返回的都不是原始類型呢?看下面這個例子:

      var obj = {

      toString: function() {

      console.log('調用了 obj.toString');

      return {};

      },

      valueOf: function() {

      console.log('調用了 obj.valueOf')

      return {};

      }

      }

      alert(obj);

      // 調用了 obj.toString

      // 調用了 obj.valueOf

      // Uncaught TypeError: Cannot convert object to primitive value

      可以發現,如果 toString 和 valueOf 方法均不可用的情況下,系統會直接返回一個錯誤。

      添加于 2017-03-07:在查證了 ECMAScript5 官方文檔后,發現上面的描述有一點問題,Object 類型轉換為 String 類型的轉換規則遠比上面復雜。轉換規則為:1.設原始值為調用 ToPrimitive 的結果;2.返回 ToString(原始值) 。關于 ToPrimitive 和 ToString 的規則可以看看官方文檔:ECMAScript5 — ToString

      Number 類型轉換

      上面描述的是 String 類型的轉換,很多時候也會發生 Number 類型的轉換:

      調用 Number() 函數,強制進行 Number 類型轉換

      調用 Math.sqrt() 這類參數需要 Number 類型的方法

      obj == 1 ,進行對比的時候

      obj + 1 , 進行運算的時候

      與 String 類型轉換相似,但是 Number 類型剛好反過來,先查詢自身的 valueOf 方法,再查詢自己 toString 方法:

      如果 valueOf 存在,且返回原始類型數據,返回 valueOf 的結果。

      如果 toString 存在,且返回原始類型數據,返回 toString 的結果。

      其他情況,拋出錯誤。

      按照上述步驟,分別嘗試一下:

      var obj = {

      valueOf: function() {

      console.log('調用 valueOf');

      return 5;

      }

      }

      console.log(obj + 1);

      // 調用 valueOf

      // 6

      var obj = {

      valueOf: function() {

      console.log('調用 valueOf');

      return {};

      },

      toString: function() {

      console.log('調用 toString');

      return 10;

      }

      }

      console.log(obj + 1);

      // 調用 valueOf

      // 調用 toString

      // 11

      var obj = {

      valueOf: function() {

      console.log('調用 valueOf');

      return {};

      },

      toString: function() {

      console.log('調用 toString');

      return {};

      }

      }

      console.log(obj + 1);

      // 調用 valueOf

      // 調用 toString

      // Uncaught TypeError: Cannot convert object to primitive value

      Boolean 轉換

      什么時候會進行布爾轉換呢:

      布爾比較時

      if(obj) , while(obj) 等判斷時

      簡單來說,除了下述 6 個值轉換結果為 false,其他全部為 true:

      undefined

      null

      -0

      0或+0

      NaN

      ”(空字符串)

      Boolean(undefined) // false

      Boolean(null) // false

      Boolean(0) // false

      Boolean(NaN) // false

      Boolean('') // false

      Function 轉換

      好,最后回到我們一開始的題目,來講講函數的轉換。

      我們定義一個函數如下:

      function test() {

      var a = 1;

      console.log(1);

      }

      如果我們僅僅是調用 test 而不是 test() ,看看會發生什么?

      

     

      可以看到,這里把我們定義的 test 函數的重新打印了一遍,其實,這里自行調用了函數的 valueOf 方法:

      

     

      我們改寫一下 test 函數的 valueOf 方法。

      test.valueOf = function() {

      console.log('調用 valueOf 方法');

      return 2;

      }

      test;

      // 輸出如下:

      // 調用 valueOf 方法

      // 2

      與 Number 轉換類似,如果函數的 valueOf 方法返回的不是一個原始類型,會繼續找到它的 toString 方法:

      test.valueOf = function() {

      console.log('調用 valueOf 方法');

      return {};

      }

      test.toString= function() {

      console.log('調用 toString 方法');

      return 3;

      }

      test;

      // 輸出如下:

      // 調用 valueOf 方法

      // 調用 toString 方法

      // 3

      破題

      再看回我正文開頭那題的答案,正是運用了函數會自行調用 valueOf 方法這個技巧,并改寫了該方法。我們稍作改變,變形如下:

      function add () {

      console.log('進入add');

      var args = Array.prototype.slice.call(arguments);

      var fn = function () {

      var arg_fn = Array.prototype.slice.call(arguments);

      console.log('調用fn');

      return add.apply(null, args.concat(arg_fn));

      }

      fn.valueOf = function () {

      console.log('調用valueOf');

      return args.reduce(function(a, b) {

      return a + b;

      })

      }

      return fn;

      }

      當調用一次 add 的時候,實際是是返回 fn 這個 function,實際是也就是返回 fn.valueOf();

      add(1);

      // 輸出如下:

      // 進入add

      // 調用valueOf

      // 1

      其實也就是相當于:

      [1].reduce(function(a, b) {

      return a + b;

      })

      // 1

      當鏈式調用兩次的時候:

      add(1)(2);

      // 輸出如下:

      // 進入add

      // 調用fn

      // 進入add

      // 調用valueOf

      // 3

      當鏈式調用三次的時候:

      add(1)(2)(3);

      // 輸出如下:

      // 進入add

      // 調用fn

      // 進入add

      // 調用fn

      // 進入add

      // 調用valueOf

      // 6

      可以看到,這里其實有一種循環。只有最后一次調用才真正調用到 valueOf,而之前的操作都是合并參數,遞歸調用本身,由于最后一次調用返回的是一個 fn 函數,所以最終調用了函數的 fn.valueOf,并且利用了 reduce 方法對所有參數求和。

      除了改寫 valueOf 方法,也可以改寫 toString 方法,所以,如果你喜歡,下面這樣也可以:

      function add () {

      var args = Array.prototype.slice.call(arguments);

      var fn = function () {

      var arg_fn = Array.prototype.slice.call(arguments);

      return add.apply(null, args.concat(arg_fn));

      }

      fn.toString = function() {

      return args.reduce(function(a, b) {

      return a + b;

      })

      }

      return fn;

      }

      這里有個規律,如果只改寫 valueOf() 或是 toString() 其中一個,會優先調用被改寫了的方法,而如果兩個同時改寫,則會像 Number 類型轉換規則一樣,優先查詢 valueOf() 方法,在 valueOf() 方法返回的是非原始類型的情況下再查詢 toString() 方法。

      后記

      像阮一峰老師所說的,“炫耀從來不是我寫作的動機,好奇才是”。本文行文過程也是我自己學習的一個過程,過程中我也遇到了很多困惑,所以即便查閱了官方文檔及大量的文章,但是錯誤及疏漏仍然在所難免,歡迎指正及給出更好的方法。

      對于類型轉換,最好還是看看 ECMAScript 規范,拒絕成為伸手黨,自己多嘗試。另外評論處有很多人提出了自己的疑問,值得一看。

      到此本文結束,如果還有什么疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。


      推薦閱讀

      JS 獲取瀏覽器和屏幕寬高信息

    網頁可見區域寬:document.body.clientWidth 網頁可見區域高:document.body.clientHeight 網頁可見區域寬:document.body.offsetWidth (包括邊線的寬) 網頁可見區域高:document.body.offsetHeight (包括邊線的>>>詳細閱讀


    本文標題:一道面試題引發的對JavaScript類型轉換的思考

    地址:http://www.jychbg.com/kaifa2/JS/34579.html

    關鍵詞: 探索發現

    樂購科技部分新聞及文章轉載自互聯網,供讀者交流和學習,若有涉及作者版權等問題請及時與我們聯系,以便更正、刪除或按規定辦理。感謝所有提供資訊的網站,歡迎各類媒體與樂購科技進行文章共享合作。

    網友點評
    自媒體專欄

    評論

    熱度

    精彩導讀
    欄目ID=71的表不存在(操作類型=0)
    免费观看人成视频在线播放

    <mark id="cjdas"></mark>

    <ruby id="cjdas"></ruby>
    <tbody id="cjdas"><delect id="cjdas"><wbr id="cjdas"></wbr></delect></tbody>
      <tbody id="cjdas"><acronym id="cjdas"></acronym></tbody>
    <listing id="cjdas"></listing>