全面分析toString與valueOf,并隨手解決掉幾道大廠必備面試題
基本上,所有JS數(shù)據(jù)類型都擁有這兩個(gè)方法,null除外。它們倆是位于原型鏈上的方法,也是為了解決javascript值運(yùn)算與顯示的問題。
valueOf 和 toString 幾乎都是在出現(xiàn)操作符(+-*/==><)時(shí)被調(diào)用(隱式轉(zhuǎn)換)。
toString
返回一個(gè)表示該對(duì)象的字符串,當(dāng)對(duì)象表示為文本值或以期望的字符串方式被引用時(shí),toString方法被自動(dòng)調(diào)用。
1. 手動(dòng)調(diào)用看看什么效果
嗯,跟介紹的一樣,沒騙人,全部都轉(zhuǎn)成了字符串。
比較特殊的地方就是,表示對(duì)象的時(shí)候,變成[object Object],表示數(shù)組的時(shí)候,就變成數(shù)組內(nèi)容以逗號(hào)連接的字符串,相當(dāng)于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. 最精準(zhǔn)的類型判斷
這種屬于更精確的判斷方式,在某種場(chǎng)合會(huì)比使用 typeof & instanceof 來的更高效和準(zhǔn)確些。
- 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. 什么時(shí)候會(huì)自動(dòng)調(diào)用呢
使用操作符的時(shí)候,如果其中一邊為對(duì)象,則會(huì)先調(diào)用toSting方法,也就是隱式轉(zhuǎn)換,然后再進(jìn)行操作。
- 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的默認(rèn)值(下面測(cè)試)
- }
- 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 這個(gè)默認(rèn)方法,那我們也可以來重寫這個(gè)方法
- 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
返回當(dāng)前對(duì)象的原始值。
具體功能與toString大同小異,同樣具有以上的自動(dòng)調(diào)用和重寫方法。
這里就沒什么好說的了,主要為兩者間的區(qū)別,有請(qǐng)繼續(xù)往下看🙊🙊
- let c = [1, 2, 3]
- let d = {a:2}
- console.log(c.valueOf()) // [1, 2, 3]
- console.log(d.valueOf()) // {a:2}
兩者區(qū)別
- 共同點(diǎn):在輸出對(duì)象時(shí)會(huì)自動(dòng)調(diào)用。
- 不同點(diǎn):默認(rèn)返回值不同,且存在優(yōu)先級(jí)關(guān)系。
二者并存的情況下,在數(shù)值運(yùn)算中,優(yōu)先調(diào)用了valueOf,字符串運(yùn)算中,優(yōu)先調(diào)用了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 => (嚴(yán)格等于不會(huì)觸發(fā)隱式轉(zhuǎn)換)
結(jié)果給人的感覺是,如果轉(zhuǎn)換為字符串時(shí)調(diào)用toString方法,如果是轉(zhuǎn)換為數(shù)值時(shí)則調(diào)用valueOf方法。
但其中的 a + '22' 很不和諧,字符串合拼應(yīng)該是調(diào)用toString方法。為了追究真相,我們需要更嚴(yán)謹(jǐn)?shù)膶?shí)驗(yàn)。
- 暫且先把 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)
發(fā)現(xiàn)有點(diǎn)不同吧?!它沒有像上面 toString 那樣統(tǒng)一規(guī)整。對(duì)于那個(gè) [object Object],我估計(jì)是從 Object 那里繼承過來的,我們?cè)偃サ羲纯础?nbsp;
- 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)
總結(jié):valueOf偏向于運(yùn)算,toString偏向于顯示。
- 在進(jìn)行對(duì)象轉(zhuǎn)換時(shí),將優(yōu)先調(diào)用toString方法,如若沒有重寫 toString,將調(diào)用 valueOf 方法;如果兩個(gè)方法都沒有重寫,則按Object的toString輸出。
- 在進(jìn)行強(qiáng)轉(zhuǎn)字符串類型時(shí),將優(yōu)先調(diào)用 toString 方法,強(qiáng)轉(zhuǎn)為數(shù)字時(shí)優(yōu)先調(diào)用 valueOf。
- 使用運(yùn)算操作符的情況下,valueOf的優(yōu)先級(jí)高于toString。
[Symbol.toPrimitive]
MDN:Symbol.toPrimitive 是一個(gè)內(nèi)置的 Symbol 值,它是作為對(duì)象的函數(shù)值屬性存在的,當(dāng)一個(gè)對(duì)象轉(zhuǎn)換為對(duì)應(yīng)的原始值時(shí),會(huì)調(diào)用此函數(shù)。
是不是有點(diǎn)懵???把它當(dāng)做一個(gè)函數(shù)就行了~~
- 作用:同valueOf()和toString()一樣,但是優(yōu)先級(jí)要高于這兩者;
- 該函數(shù)被調(diào)用時(shí),會(huì)被傳遞一個(gè)字符串參數(shù)
hint
,表示當(dāng)前運(yùn)算的模式,一共有三種模式:
- string:字符串類型
- number:數(shù)字類型
- default:默認(rèn)
下面來看看實(shí)現(xiàn)吧:
- 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")
比較特殊的是(+)拼接符,這個(gè)屬于default的模式。
劃重點(diǎn):此方法不兼容IE,尷尬到我不想寫出來了~~
面試題分析
以下幾道大廠必考的面試題,完美呈現(xiàn)出 toString 與 valueOf 的作用。
1. a===1&&a===2&&a===3 為 true
雙等號(hào)(==):會(huì)觸發(fā)隱式類型轉(zhuǎn)換,所以可以使用 valueOf 或者 toString 來實(shí)現(xiàn)。
每次判斷都會(huì)觸發(fā)valueOf方法,同時(shí)讓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!");
- }
全等(===):嚴(yán)格等于不會(huì)進(jìn)行隱式轉(zhuǎn)換,這里使用 Object.defineProperty 數(shù)據(jù)劫持的方法來實(shí)現(xiàn)
- let value = 1;
- Object.defineProperty(window, 'a', {
- get() {
- return value++
- }
- })
- if (a === 1 && a === 2 && a === 3) {
- console.log("Hi Libai!")
- }
上面我們就是劫持全局window上面的a,當(dāng)a每一次做判斷的時(shí)候都會(huì)觸發(fā)get屬性獲取值,并且每一次獲取值都會(huì)觸發(fā)一次函數(shù)實(shí)行一次自增,判斷三次就自增三次,所以最后會(huì)讓公式成立。
- 注:defineProperty 可參考這篇文章學(xué)習(xí),點(diǎn)我進(jìn)入傳送門
- 自:大廠面試題分享:如何讓(a===1&&a===2&&a===3)的值為true?
2. 實(shí)現(xiàn)一個(gè)無限累加函數(shù)
問題:用 JS 實(shí)現(xiàn)一個(gè)無限累加的函數(shù) 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() { // 只在最后一次調(diào)用
- return a;
- }
- return sum; // 返回一個(gè)函數(shù)
- }
- add(1) // 1
- add(1)(2) // 3
- add(1)(2)(3) // 6
- add(1)(2)(3)(4) // 10
- add函數(shù)內(nèi)部定義sum函數(shù)并返回,實(shí)現(xiàn)連續(xù)調(diào)用
- sum函數(shù)形成了一個(gè)閉包,每次調(diào)用進(jìn)行累加值,再返回當(dāng)前函數(shù)sum
- add()每次都會(huì)返回一個(gè)函數(shù)sum,直到最后一個(gè)沒被調(diào)用,默認(rèn)會(huì)觸發(fā)toString方法,所以我們這里重寫toString方法,并返回累計(jì)的最終值a
這樣說才能理解:
add(10): 執(zhí)行函數(shù)add(10),返回了sum函數(shù),注意這一次沒有調(diào)用sum,默認(rèn)執(zhí)行sum.toString方法。所以輸出10;
add(10)(20): 執(zhí)行函數(shù)add(10),返回sum(此時(shí)a為10),再執(zhí)行sum(20),此時(shí)a為30,返回sum,最后調(diào)用sum.toString()輸出30。add(10)(20)...(n)依次類推。
3. 柯里化實(shí)現(xiàn)多參累加
這里是上面累加的升級(jí)版,實(shí)現(xiàn)多參數(shù)傳遞累加。
- add(1)(3,4)(3,5) // 16
- add(2)(2)(3,5) // 12
- function add(){
- // 1 把所有參數(shù)轉(zhuǎn)換成數(shù)組
- let args = Array.prototype.slice.call(arguments)
- // 2 再次調(diào)用add函數(shù),傳遞合并當(dāng)前與之前的參數(shù)
- let fn = function() {
- let arg_fn = Array.prototype.slice.call(arguments)
- return add.apply(null, args.concat(arg_fn))
- }
- // 3 最后默認(rèn)調(diào)用,返回合并的值
- 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;
- }