全面分析toString與valueOf,並隨手解決掉幾道大廠必備面試題

Hello,你們好,我是李白~~javascript

最近深圳的天氣是變化無窮的,時而傾盆大雨,時而豔陽高照,多但願能有個幾天是連綿不絕地下雨,那該多好啊~~前端

讓人恐懼的夏天也許就過去了,沒人知道你在這個夏天、這個今年發生什麼,可是我知道。vue

知道你是努力的,也有多是緩慢的。不過面對秋天,面對即將到來的國慶小假期,還請繼續努力吧,川流不息的不止生命,還有時間,加油~~java

今天,我是做者,也是讀者,爲本身交做業,也爲技術學習的路上交做業~~git

好了,進入正題,先理解這兩個方法是作什麼的:github

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

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

toString

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

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

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

比較特殊的地方就是,表示對象的時候,變成[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方法,也就是隱式轉換,而後再進行操做。

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的默認值(下面測試)
}
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屬性獲取值,而且每一次獲取值都會觸發一次函數實行一次自增,判斷三次就自增三次,因此最後會讓公式成立。

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;
}
複製代碼

總結

小白寫文,如寫得很差,還請給個建議~~ 若是對你有幫助的話,還請點個贊~~

更多信息分享:

相關文章
相關標籤/搜索