面試靈魂拷問之JS,請問你頭皮發麻沒有?

JS數據類型之問—概念篇

1.JS原始數據類型有哪些?引用數據類型有哪些?
在 JS 中,存在着 7 種原始值,分別是:

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol
  • bigint
    引用數據類型:
    對象Object(包含普通對象-Object,數組對象-Array,正則對象-RegExp,日期對象-Date,數學函數-Math)
    函數Function
    2.說出下面運行的結果,解釋原因。
 function test(person) {
   person.age = 26
   person = {
     name: 'hzj',
     age: 18
   }
   return person
 }
 const p1 = {
   name: 'fyq',
   age: 19
 }
 const p2 = test(p1)
 console.log(p1) // -> ?
 console.log(p2) // -> ?

結果:

 p1:{name: “fyq”, age: 26}
 p2:{name: “hzj”, age: 18}

原因: 在函數傳參的時候傳遞的是對象在堆中的內存地址值,test函數中的實參person是p1對象的內存地址,通過調用person.age = 26確實改變了p1的值,但隨後person變成了另一塊內存空間的地址,並且在最後將這另外一份內存空間的地址返回,賦給了p2。
3.null是對象嗎?爲什麼?
結論: null不是對象。
解釋: 雖然 typeof null 會輸出 object,但是這只是 JS 存在的一個悠久 Bug。在 JS 的最初版本中使用的是 32 位系統,爲了性能考慮使用低位存儲變量的類型信息,000 開頭代表是對象然而 null 表示爲全零,所以將它錯誤的判斷爲 object 。
4.'1'.toString()爲什麼可以調用?
其實在這個語句運行的過程中做了這樣幾件事情:

 var s = new String('1');
 s.toString();
 s = null; 

第一步: 創建String類實例。
第二步: 調用實例方法。
第三步: 執行完方法立即銷燬這個實例。
整個過程體現了基本包裝類型的性質,而基本包裝類型恰恰屬於基本數據類型,包括Boolean, Number和String。

參考:《JavaScript高級程序設計(第三版)》P118

5.0.1+0.2爲什麼不等於0.3?
0.1和0.2在轉換成二進制後會無限循環,由於標準位數的限制後面多餘的位數會被截掉,此時就已經出現了精度的損失,相加後因浮點數小數位的限制而截斷的二進制數字在轉換爲十進制就會變成0.30000000000000004。

第二篇: JS數據類型之問——檢測篇

1. typeof 是否能正確判斷類型?
對於原始類型來說,除了 null 都可以調用typeof顯示正確的類型。

 typeof 1 // 'number'
 typeof '1' // 'string'
 typeof undefined // 'undefined'
 typeof true // 'boolean'
 typeof Symbol() // 'symbol' 

但對於引用數據類型,除了函數之外,都會顯示"object"。

 typeof [] // 'object'
 typeof {} // 'object'
 typeof console.log // 'function' 

因此採用typeof判斷對象數據類型是不合適的,採用instanceof會更好,instanceof的原理是基於原型鏈的查詢,只要處於原型鏈中,判斷永遠爲true

 const Person = function() {}
 const p1 = new Person()
 p1 instanceof Person // true

 var str1 = 'hello world'
 str1 instanceof String // false

 var str2 = new String('hello world')
 str2 instanceof String // true

2. instanceof能否判斷基本數據類型?
能。比如下面這種方式:

 class PrimitiveNumber {
   static [Symbol.hasInstance](x) {
     return typeof x === 'number'
   }
 }
 console.log(111 instanceof PrimitiveNumber) // true 

如果你不知道Symbol,可以看看MDN上關於hasInstance的解釋。
其實就是自定義instanceof行爲的一種方式,這裏將原有的instanceof方法重定義,換成了typeof,因此能夠判斷基本數據類型。

3. 能不能手動實現一下instanceof的功能
核心: 原型鏈的向上查找。

 function myInstanceof(left, right) {
     //基本數據類型直接返回false
 if(typeof left !== 'object' || left === null) return false;
     //getProtypeOf是Object對象自帶的一個方法,能夠拿到參數的原型對象
 let proto = Object.getPrototypeOf(left);
     while(true) {
         //查找到盡頭,還沒找到
 if(proto == null) return false;
         //找到相同的原型對象
 if(proto == right.prototype) return true;
         proto = Object.getPrototypeof(proto);
     }
 }  

測試:

 console.log(myInstanceof("111", String)); //false
 console.log(myInstanceof(new String("111"), String));//true 

4. Object.is和===的區別?
Object在嚴格等於的基礎上修復了一些特殊情況下的失誤,具體來說就是+0和-0,NaN和NaN。 源碼如下:

 function is(x, y) {
   if (x === y) {
     //運行到1/x === 1/y的時候x和y都爲0,但是1/+0 = +Infinity, 1/-0 = -Infinity, 是不一樣的
 return x !== 0 || y !== 0 || 1 / x === 1 / y;
   } else {
     //NaN===NaN是false,這是不對的,我們在這裏做一個攔截,x !== x,那麼一定是 NaN, y 同理
 //兩個都是NaN的時候返回true
 return x !== x && y !== y;
   }    

第三篇: JS數據類型之問——轉換篇

1. [] == ![]結果是什麼?爲什麼?
解析:
== 中,左右兩邊都需要轉換爲數字然後進行比較。
[]轉換爲數字爲0。
![] 首先是轉換爲布爾值,由於[]作爲一個引用類型轉換爲布爾值爲true,
因此![]爲false,進而在轉換成數字,變爲0。
0 == 0 , 結果爲true

2. JS中類型轉換有哪幾種?

JS中,類型轉換隻有三種:

  • 轉換成數字
  • 轉換成布爾值
  • 轉換成字符串

轉換具體規則如下:

注意"Boolean 轉字符串"這行結果指的是 true 轉字符串的例子
面試靈魂拷問之JS,請問你頭皮發麻沒有?

3. == 和 ===有什麼區別?

===叫做嚴格相等,是指:左右兩邊不僅值要相等,類型也要相等,例如'1'===1的結果是false,因爲一邊是string,另一邊是number。

==不像===那樣嚴格,對於一般情況,只要值相等,就返回true,但==還涉及一些類型轉換,它的轉換規則如下:

  • 兩邊的類型是否相同,相同的話就比較值的大小,例如1==2,返回false
  • 判斷的是否是null和undefined,是的話就返回true
  • 判斷的類型是否是String和Number,是的話,把String類型轉換成Number,再進行比較
  • 判斷其中一方是否是Boolean,是的話就把Boolean轉換成Number,再進行比較
  • 如果其中一方爲Object,且另一方爲String、Number或者Symbol,會將Object轉換成字符串,再進行比較
    console.log({a: 1} == true);//false
    console.log({a: 1} == "[object Object]");//true 

4. 對象轉原始類型是根據什麼流程運行的?

對象轉原始類型,會調用內置的[ToPrimitive]函數,對於該函數而言,其邏輯如下:

  1. 如果Symbol.toPrimitive()方法,優先調用再返回
  2. 調用valueOf(),如果轉換爲原始類型,則返回
  3. 調用toString(),如果轉換爲原始類型,則返回
  4. 如果都沒有返回原始類型,會報錯
    var obj = {
    value: 3,
    valueOf() {
     return 4;
    },
    toString() {
     return '5'
    },
    [Symbol.toPrimitive]() {
     return 6
    }
    }
    console.log(obj + 1); // 輸出7

5. 如何讓if(a == 1 && a == 2)條件成立?
其實就是上一個問題的應用。

 var a = {
   value: 0,
   valueOf: function() {
     this.value++;
     return this.value;
   }
 };
 console.log(a == 1 && a == 2);//true 

第四篇: 談談你對閉包的理解

什麼是閉包?

紅寶書(p178)上對於閉包的定義:閉包是指有權訪問另外一個函數作用域中的變量的函數.

MDN 對閉包的定義爲:閉包是指那些能夠訪問自由變量的函數。 (其中自由變量,指在函數中使用的,但既不是函數參數arguments也不是函數的局部變量的變量,其實就是另外一個函數作用域中的變量。)

閉包產生的原因?
首先要明白作用域鏈的概念,其實很簡單,在ES5中只存在兩種作用域————全局作用域和函數作用域,當訪問一個變量時,解釋器會首先在當前作用域查找標示符,如果沒有找到,就去父作用域找,直到找到該變量的標示符或者不在父作用域中,這就是作用域鏈,值得注意的是,每一個子函數都會拷貝上級的作用域,形成一個作用域的鏈條。 比如:

 var a = 1;
 function f1() {
   var a = 2
   function f2() {
     var a = 3;
     console.log(a);//3
   }
 } 

在這段代碼中,f1的作用域指向有全局作用域(window)和它本身,而f2的作用域指向全局作用域(window)、f1和它本身。而且作用域是從最底層向上找,直到找到全局作用域window爲止,如果全局還沒有的話就會報錯。就這麼簡單一件事情!
閉包產生的本質就是,當前環境中存在指向父級作用域的引用。還是舉上面的例子:

 function f1() {
   var a = 2
   function f2() {
     console.log(a);//2
   }
   return f2;
 }
 var x = f1();
 x(); 

這裏x會拿到父級作用域中的變量,輸出2。因爲在當前環境中,含有對f2的引用,f2恰恰引用了window、f1和f2的作用域。因此f2可以訪問到f1的作用域的變量。
那是不是隻有返回函數纔算是產生了閉包呢?
回到閉包的本質,我們只需要讓父級作用域的引用存在即可,因此我們還可以這麼做:

 var f3;
 function f1() {
   var a = 2
   f3 = function() {
     console.log(a);
   }
 }
 f1();
 f3(); 

讓f1執行,給f3賦值後,等於說現在f3擁有了window、f1和f3本身這幾個作用域的訪問權限,還是自底向上查找,最近是在f1中找到了a,因此輸出2。
在這裏是外面的變量f3存在着父級作用域的引用,因此產生了閉包,形式變了,本質沒有改變。
閉包有哪些表現形式?
明白了本質之後,我們就來看看,在真實的場景中,究竟在哪些地方能體現閉包的存在?
返回一個函數。剛剛已經舉例。

作爲函數參數傳遞

 var a = 1;
 function foo(){ 
  var a = 2;
   function baz(){
     console.log(a);
   }
   bar(baz);
 }
 function bar(fn){
   // 這就是閉包
   fn();
 }
 // 輸出2,而不是1
 foo(); 

在定時器、事件監聽、Ajax請求、跨窗口通信、Web Workers或者任何異步中,只要使用了回調函數,實際上就是在使用閉包。

以下的閉包保存的僅僅是window和當前作用域。

 // 定時器
 setTimeout(function timeHandler(){
   console.log('111');
 },100)

  // 事件監聽
 $('#app').click(function(){
   console.log('DOM Listener');
 })

IIFE(立即執行函數表達式)創建閉包, 保存了全局作用域window和當前函數的作用域,因此可以全局的變量。

 var a = 2;
 (function IIFE(){
   // 輸出2
 console.log(a);
 })(); 

如何解決下面的循環輸出問題?

 for(var i = 1; i <= 5; i ++){
   setTimeout(function timer(){
     console.log(i)
   }, 0)
 }  

爲什麼會全部輸出6?如何改進,讓它輸出1,2,3,4,5?(方法越多越好)
因爲setTimeout爲宏任務,由於JS中單線程eventLoop機制,在主線程同步任務執行完後纔去執行宏任務,因此循環結束後setTimeout中的回調才依次執行,但輸出i的時候當前作用域沒有,往上一級再找,發現了i,此時循環已經結束,i變成了6。因此會全部輸出6。
解決方法:
1、利用IIFE(立即執行函數表達式)當每次for循環時,把此時的i變量傳遞到定時器中

 for(var i = 1;i <= 5;i++){
   (function(j){
     setTimeout(function timer(){
       console.log(j)
     }, 0)
   })(i)
 } 

2、給定時器傳入第三個參數, 作爲timer函數的第一個函數參數

 for(var i=1;i<=5;i++){
   setTimeout(function timer(j){
     console.log(j)
   }, 0, i)
 }

3、使用ES6中的let

 for(let i = 1; i <= 5; i++){
   setTimeout(function timer(){
     console.log(i)
   },0)
 }  

let使JS發生革命性的變化,讓JS有函數作用域變爲了塊級作用域,用let後作用域鏈不復存在。代碼的作用域以塊級爲單位,以上面代碼爲例:

 // i = 1
 {
   setTimeout(function timer(){
     console.log(1)
   },0)
 }
 // i = 2
 {
   setTimeout(function timer(){
     console.log(2)
   },0)
 }
 // i = 3
 ... 

因此能輸出正確的結果。

第五篇: 談談你對原型鏈的理解

1.原型對象和構造函數有何關係?
在JavaScript中,每當定義一個函數數據類型(普通函數、類)時候,都會天生自帶一個prototype屬性,這個屬性指向函數的原型對象。
當函數經過new調用時,這個函數就成爲了構造函數,返回一個全新的實例對象,這個實例對象有一個proto屬性,指向構造函數的原型對象。

![](https://s1.51cto.com/images/blog/201910/22/e8aaa00fe932ce3f40821a825274318e.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=) 

2.能不能描述一下原型鏈?
JavaScript對象通過prototype指向父類對象,直到指向Object對象爲止,這樣就形成了一個原型指向的鏈條, 即原型鏈。
面試靈魂拷問之JS,請問你頭皮發麻沒有?

對象的 hasOwnProperty() 來檢查對象自身中是否含有該屬性
使用 in 檢查對象中是否含有某個屬性時,如果對象中沒有但是原型鏈中有,也會返回 true

第六篇: JS如何實現繼承?

第一種: 藉助call

 function Parent1(){
     this.name = 'parent1';
   }
   function Child1(){
     Parent1.call(this);
     this.type = 'child1'
   }
   console.log(new Child1); 

這樣寫的時候子類雖然能夠拿到父類的屬性值,但是問題是父類原型對象中一旦存在方法那麼子類無法繼承。那麼引出下面的方法。

 function Parent2() {
     this.name = 'parent2';
     this.play = [1, 2, 3]
   }
   function Child2() {
     this.type = 'child2';
   }
   Child2.prototype = new Parent2();

   console.log(new Child2());

看似沒有問題,父類的方法和屬性都能夠訪問,但實際上有一個潛在的不足。舉個例子:

 var s1 = new Child2();
 var s2 = new Child2();
 s1.play.push(4);
 console.log(s1.play, s2.play);   

可以看到控制檯:
面試靈魂拷問之JS,請問你頭皮發麻沒有?

明明我只改變了s1的play屬性,爲什麼s2也跟着變了呢?很簡單,因爲兩個實例使用的是同一個原型對象。
那麼還有更好的方式麼?
第三種:將前兩種組合

 function Parent3 () {
     this.name = 'parent3';
     this.play = [1, 2, 3];
   }
   function Child3() {
     Parent3.call(this);
     this.type = 'child3';
   }
   Child3.prototype = new Parent3();
   var s3 = new Child3();
   var s4 = new Child3();
   s3.play.push(4);
   console.log(s3.play, s4.play);

可以看到控制檯:
面試靈魂拷問之JS,請問你頭皮發麻沒有?

之前的問題都得以解決。但是這裏又徒增了一個新問題,那就是Parent3的構造函數會多執行了一次(Child3.prototype = new Parent3();)。這是我們不願看到的。那麼如何解決這個問題?
第四種: 組合繼承的優化1

   function Parent4 () {
     this.name = 'parent4';
     this.play = [1, 2, 3];
   }
   function Child4() {
     Parent4.call(this);
     this.type = 'child4';
   }
   Child4.prototype = Parent4.prototype; 

這裏讓將父類原型對象直接給到子類,父類構造函數只執行一次,而且父類屬性和方法均能訪問,但是我們來測試一下:

 var s3 = new Child4();
 var s4 = new Child4();
 console.log(s3) 

面試靈魂拷問之JS,請問你頭皮發麻沒有?

子類實例的構造函數是Parent4,顯然這是不對的,應該是Child4。

第五種(最推薦使用): 組合繼承的優化1

 function Parent5 () {
     this.name = 'parent5';
     this.play = [1, 2, 3];
   }
   function Child5() {
     Parent5.call(this);
     this.type = 'child5';
   }
   Child5.prototype = Object.create(Parent5.prototype);
   Child5.prototype.constructor = Child5; 

這是最推薦的一種方式,接近完美的繼承,它的名字也叫做寄生組合繼承。

ES6的extends被編譯後的JavaScript代碼
ES6的代碼最後都是要在瀏覽器上能夠跑起來的,這中間就利用了babel這個編譯工具,將ES6的代碼編譯成ES5讓一些不支持新語法的瀏覽器也能運行。
那最後編譯成了什麼樣子呢?

 function _possibleConstructorReturn (self, call) {
        // ...
 return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
  }
  function _inherits (subClass, superClass) {
      // ...
 //看到沒有
        subClass.prototype = Object.create(superClass && superClass.prototype, {
                constructor: {
                        value: subClass,
                        enumerable: false,
                        writable: true,
                        configurable: true
                }
        });
        if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
  }
   var Parent = function Parent () {
        // 驗證是否是 Parent 構造出來的 this
        _classCallCheck(this, Parent);
 };
  var Child = (function (_Parent) {
        _inherits(Child, _Parent);
        function Child () {
                _classCallCheck(this, Child);
                return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
        }

return Child;
 }(Parent)); 

核心是_inherits函數,可以看到它採用的依然也是第五種方式————寄生組合繼承方式,同時證明了這種方式的成功。不過這裏加了一個Object.setPrototypeOf(subClass, superClass),這是用來幹啥的呢?
答案是用來繼承父類的靜態方法。這也是原來的繼承方式疏忽掉的地方。

追問: 面向對象的設計一定是好的設計嗎?

不一定。從繼承的角度說,這一設計是存在巨大隱患的。
從設計思想上談談繼承本身的問題
假如現在有不同品牌的車,每輛車都有drive、music、addOil這三個方法。

 class Car{
   constructor(id)
 {
     this.id = id;
   }
   drive(){
     console.log("wuwuwu!");
   }
   music(){
     console.log("lalala!")
   }
   addOil(){
     console.log("哦喲!")
   }
 }
 class otherCar extends Car{} 

現在可以實現車的功能,並且以此去擴展不同的車。
但是問題來了,新能源汽車也是車,但是它並不需要addOil(加油)。
如果讓新能源汽車的類繼承Car的話,也是有問題的,俗稱"大猩猩和香蕉"的問題。大猩猩手裏有香蕉,但是我現在明明只需要香蕉,卻拿到了一隻大猩猩。也就是說加油這個方法,我現在是不需要的,但是由於繼承的原因,也給到子類了。

繼承的最大問題在於:無法決定繼承哪些屬性,所有屬性都得繼承。

當然你可能會說,可以再創建一個父類啊,把加油的方法給去掉,但是這也是有問題的,一方面父類是無法描述所有子類的細節情況的,爲了不同的子類特性去增加不同的父類,代碼勢必會大量重複,另一方面一旦子類有所變動,父類也要進行相應的更新,代碼的耦合性太高,維護性不好。
那如何來解決繼承的諸多問題呢?
用組合,這也是當今編程語法發展的趨勢,比如golang完全採用的是面向組合的設計方式。
顧名思義,面向組合就是先設計一系列零件,然後將這些零件進行拼裝,來形成不同的實例或者類。

 function drive(){
   console.log("wuwuwu!");
 }
 function music(){
   console.log("lalala!")
 }
 function addOil(){
   console.log("哦喲!")
 }
 let car = compose(drive, music, addOil);
 let newEnergyCar = compose(drive, music);

代碼乾淨,複用性也很好。這就是面向組合的設計方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章