全面分析toString與valueOf

轉自原文:https://juejin.im/post/687321524380421326javascript

基本上,全部JS數據類型都擁有這兩個方法,null除外。它們倆是位於原型鏈上的方法,也是爲了解決javascript值運算與顯示的問題。java

valueOftoString 幾乎都是在出現操做符(+-*/==><)時被調用(隱式轉換)。web

toString

返回一個表示該對象的字符串,當對象表示爲文本值或以指望的字符串方式被引用時,toString方法被自動調用。面試

1. 手動調用看看什麼效果

嗯,跟介紹的同樣,沒騙人,所有都轉成了字符串。數組

比較特殊的地方就是,表示對象的時候,變成[object Object],表示數組的時候,就變成數組內容以逗號鏈接的字符串,至關於Array.join(',')微信

let a = {}
let b = [1, 2, 3]
let c = '123'
let d = function(){ console.log('fn') }

console.log(a.toString())   // '[object Object]'
console.log(b.toString())   // '1,2,3'
console.log(c.toString())   // '123'
console.log(d.toString())   // 'function(){ console.log('fn') }'

2. 最精準的類型判斷

這種屬於更精確的判斷方式,在某種場合會比使用 typeof & instanceof 來的更高效和準確些。閉包

toString.call(()=>{})       // [object Function]
toString.call({})           // [object Object]
toString.call([])           // [object Array]
toString.call('')           // [object String]
toString.call(22)           // [object Number]
toString.call(undefined)    // [object undefined]
toString.call(null)         // [object null]
toString.call(new Date)     // [object Date]
toString.call(Math)         // [object Math]
toString.call(window)       // [object Window]

3. 何時會自動調用呢

使用操做符的時候,若是其中一邊爲對象,則會先調用toSting方法,也就是隱式轉換,而後再進行操做。app

let c = [1, 2, 3]
let d = {a:2}
Object.prototype.toString = function(){
    console.log('Object')
}
Array.prototype.toString = function(){
    console.log('Array')
    return this.join(',')   // 返回toString的默認值(下面測試)
}
Number.prototype.toString = function(){
    console.log('Number')
}
String.prototype.toString = function(){
    console.log('String')
}
console.log(2 + 1)  // 3
console.log('s')    // 's'
console.log('s'+2)  // 's2'
console.log(c < 2)  // false        (一次 => 'Array')
console.log(c + c)  // "1,2,31,2,3" (兩次 => 'Array')
console.log(d > d)  // false        (兩次 => 'Object')

4. 重寫toString方法

既然知道了有 toString 這個默認方法,那咱們也能夠來重寫這個方法編輯器

class A {
    constructor(count) {
        this.count = count
    }
    toString() {
        return '我有這麼多錢:' + this.count
    }
}
let a = new A(100)

console.log(a)              // A {count: 100}
console.log(a.toString())   // 我有這麼多錢:100
console.log(a + 1)          // 我有這麼多錢:1001

Nice.函數

valueOf

返回當前對象的原始值。

具體功能與toString大同小異,一樣具備以上的自動調用和重寫方法。

這裏就沒什麼好說的了,主要爲二者間的區別,有請繼續往下看🙊🙊

let c = [1, 2, 3]
let d = {a:2}

console.log(c.valueOf())    // [1, 2, 3]
console.log(d.valueOf())    // {a:2}

二者區別

  • 共同點:在輸出對象時會自動調用。
  • 不一樣點: 默認返回值不一樣,且存在優先級關係。

兩者並存的狀況下,在數值運算中,優先調用了valueOf,字符串運算中,優先調用了toString

看代碼方可知曉:

class A {
    valueOf() {
        return 2
    }
    toString() {
        return '哈哈哈'
    }
}
let a = new A()

console.log(String(a))  // '哈哈哈'   => (toString)
console.log(Number(a))  // 2         => (valueOf)
console.log(a + '22')   // '222'     => (valueOf)
console.log(a == 2)     // true      => (valueOf)
console.log(a === 2)    // false     => (嚴格等於不會觸發隱式轉換)

結果給人的感受是,若是轉換爲字符串時調用toString方法,若是是轉換爲數值時則調用valueOf方法。

但其中的 a + '22' 很不和諧,字符串合拼應該是調用toString方法。爲了追究真相,咱們須要更嚴謹的實驗。

  • 暫且先把 valueOf 方法去掉
class A {
    toString() {
        return '哈哈哈'
    }
}
let a = new A()

console.log(String(a))  // '哈哈哈'     => (toString)
console.log(Number(a))  // NaN         => (toString)
console.log(a + '22')   // '哈哈哈22'   => (toString)
console.log(a == 2)     // false       => (toString)
  • 去掉 toString 方法看看
class A {
    valueOf() {
        return 2
    }
}
let a = new A()

console.log(String(a))  // '[object Object]'    => (toString)
console.log(Number(a))  // 2                    => (valueOf)
console.log(a + '22')   // '222'                => (valueOf)
console.log(a == 2)     // true                 => (valueOf)

發現有點不一樣吧?!它沒有像上面 toString 那樣統一規整。對於那個 [object Object],我估計是從 Object 那裏繼承過來的,咱們再去掉它看看。

class A {
    valueOf() {
        return 2
    }
}
let a = new A()

Object.prototype.toString = null; 

console.log(String(a))  // 2        => (valueOf)
console.log(Number(a))  // 2        => (valueOf)
console.log(a + '22')   // '222'    => (valueOf)
console.log(a == 2)     // true     => (valueOf)

總結:valueOf偏向於運算,toString偏向於顯示。

  1. 在進行對象轉換時,將優先調用 toString方法,如若沒有重寫 toString,將調用 valueOf 方法;若是兩個方法都沒有重寫,則按 ObjecttoString輸出。
  2. 在進行 強轉字符串類型時,將優先調用 toString 方法,強轉爲數字時優先調用 valueOf
  3. 使用運算操做符的狀況下, valueOf的優先級高於 toString

Symbol.toPrimitive

MDN:Symbol.toPrimitive 是一個內置的 Symbol 值,它是做爲對象的函數值屬性存在的,當一個對象轉換爲對應的原始值時,會調用此函數。

是否是有點懵???把它當作一個函數就好了~~

  • 做用:同 valueOf()toString()同樣,可是優先級要高於這二者;
  • 該函數被調用時,會被傳遞一個字符串參數
        hint
,表示當前運算的模式,一共有三種模式:
  • string:字符串類型

    number:數字類型

    default:默認

下面來看看實現吧:

class A {
    constructor(count) {
        this.count = count
    }
    valueOf() {
        return 2
    }
    toString() {
        return '哈哈哈'
    }
    // 我在這裏
    [Symbol.toPrimitive](hint) {
        if (hint == "number") {
            return 10;
        }
        if (hint == "string") {
            return "Hello Libai";
        }
        return true;
    }
}

const a = new A(10)

console.log(`${a}`)     // 'Hello Libai' => (hint == "string")
console.log(String(a))  // 'Hello Libai' => (hint == "string")
console.log(+a)         // 10            => (hint == "number")
console.log(a * 20)     // 200           => (hint == "number")
console.log(a / 20)     // 0.5           => (hint == "number")
console.log(Number(a))  // 10            => (hint == "number")
console.log(a + '22')   // 'true22'      => (hint == "default")
console.log(a == 10)     // false        => (hint == "default")

比較特殊的是(+)拼接符,這個屬於default的模式。

劃重點:此方法不兼容IE,尷尬到我不想寫出來了~~

面試題分析

如下幾道大廠必考的面試題,完美呈現出 toStringvalueOf 的做用。

1. a===1&&a===2&&a===3 爲 true

雙等號(==):會觸發隱式類型轉換,因此可使用 valueOf 或者 toString 來實現。

每次判斷都會觸發valueOf方法,同時讓value+1,才能使得下次判斷成立。

class A {
    constructor(value) {
        this.value = value;
    }
    valueOf() {
        return this.value++;
    }
}
const a = new A(1);
if (a == 1 && a == 2 && a == 3) {
    console.log("Hi Libai!");
}

全等(===):嚴格等於不會進行隱式轉換,這裏使用 Object.defineProperty 數據劫持的方法來實現

let value = 1;
Object.defineProperty(window, 'a', {
    get() {
        return value++
    }
})
if (a === 1 && a === 2 && a === 3) {
    console.log("Hi Libai!")
}

上面咱們就是劫持全局window上面的a,當a每一次作判斷的時候都會觸發get屬性獲取值,而且每一次獲取值都會觸發一次函數實行一次自增,判斷三次就自增三次,因此最後會讓公式成立。

  • 注: defineProperty 可參考這篇文章學習,點我進入傳送門
  • 自:大廠面試題分享:如何讓(a===1&&a===2&&a===3)的值爲true?

2. 實現一個無限累加函數

問題:用 JS 實現一個無限累加的函數 add,示例以下:

add(1); // 1
add(1)(2);  // 3
add(1)(2)(3); // 6
add(1)(2)(3)(4); // 10 

// 以此類推
function add(a) {
    function sum(b) { // 使用閉包
        a = b ? a + b : a; // 累加
        return sum;
    }
    sum.toString = function() { // 只在最後一次調用
        return a;
    }
    return sum; // 返回一個函數
}

add(1)              // 1
add(1)(2)           // 3
add(1)(2)(3)        // 6
add(1)(2)(3)(4)     // 10 
  • add函數內部定義 sum函數並返回,實現連續調用
  • sum函數造成了一個閉包,每次調用進行累加值,再返回當前函數 sum
  • add()每次都會返回一個函數 sum,直到最後一個沒被調用,默認會觸發 toString方法,因此咱們這裏重寫 toString方法,並返回累計的最終值 a

這樣說才能理解:

add(10): 執行函數add(10),返回了sum函數,注意這一次沒有調用sum,默認執行sum.toString方法。因此輸出10

add(10)(20): 執行函數add(10),返回sum(此時a爲10),再執行sum(20),此時a爲30,返回sum,最後調用sum.toString()輸出30。add(10)(20)...(n)依次類推。

3. 柯里化實現多參累加

這裏是上面累加的升級版,實現多參數傳遞累加。

add(1)(3,4)(3,5)    // 16
add(2)(2)(3,5)      // 12
function add(){
    // 1 把全部參數轉換成數組
    let args = Array.prototype.slice.call(arguments)
    // 2 再次調用add函數,傳遞合併當前與以前的參數
    let fn = function() {
        let arg_fn = Array.prototype.slice.call(arguments)
        return add.apply(null, args.concat(arg_fn))
    }
    // 3 最後默認調用,返回合併的值
    fn.toString = function() {
        return args.reduce(function(a, b) {
            return a + b
        })
    }
    return fn
}

// ES6寫法
function add () {
    let args = [...arguments];
    let fn = function(){
        return add.apply(null, args.concat([...arguments]))
    } 
    fn.toString = () => args.reduce((a, b) => a + b)
    return fn;
}


本文分享自微信公衆號 - JavaScript忍者祕籍(js-obok)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索