1.JS原始數據類型有哪些?引用數據類型有哪些?
在 JS 中,存在着 7 種原始值,分別是:golang
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。babel
1. typeof 是否能正確判斷類型?
對於原始類型來講,除了 null 均可以調用typeof顯示正確的類型。markdown
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的原理是基於原型鏈的查詢,只要處於原型鏈中,判斷永遠爲trueapp
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; }
1. [] == ![]結果是什麼?爲何?
解析:
== 中,左右兩邊都須要轉換爲數字而後進行比較。
[]轉換爲數字爲0。
![] 首先是轉換爲布爾值,因爲[]做爲一個引用類型轉換爲布爾值爲true,
所以![]爲false,進而在轉換成數字,變爲0。
0 == 0 , 結果爲true
2. JS中類型轉換有哪幾種?
JS中,類型轉換隻有三種:
轉換具體規則以下:
注意"Boolean 轉字符串"這行結果指的是 true 轉字符串的例子
3. == 和 ===有什麼區別?
===叫作嚴格相等,是指:左右兩邊不只值要相等,類型也要相等,例如'1'===1的結果是false,由於一邊是string,另外一邊是number。
==不像===那樣嚴格,對於通常狀況,只要值相等,就返回true,但==還涉及一些類型轉換,它的轉換規則以下:
console.log({a: 1} == true);//false console.log({a: 1} == "[object Object]");//true
4. 對象轉原始類型是根據什麼流程運行的?
對象轉原始類型,會調用內置的[ToPrimitive]函數,對於該函數而言,其邏輯以下:
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。
解決方法:
一、利用IIFE(當即執行函數表達式)當每次for循環時,把此時的i變量傳遞到定時器中
for(var i = 1;i <= 5;i++){ (function(j){ setTimeout(function timer(){ console.log(j) }, 0) })(i) }
二、給定時器傳入第三個參數, 做爲timer函數的第一個函數參數
for(var i=1;i<=5;i++){ setTimeout(function timer(j){ console.log(j) }, 0, i) } 三、使用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對象爲止,這樣就造成了一個原型指向的鏈條, 即原型鏈。
對象的 hasOwnProperty() 來檢查對象自身中是否含有該屬性
使用 in 檢查對象中是否含有某個屬性時,若是對象中沒有可是原型鏈中有,也會返回 true
第一種: 藉助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);
能夠看到控制檯:
明明我只改變了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);
能夠看到控制檯:
以前的問題都得以解決。可是這裏又徒增了一個新問題,那就是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)
子類實例的構造函數是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);
代碼乾淨,複用性也很好。這就是面向組合的設計方式。