面試官:說說 JavaScript 中的基本類型有哪些?以及各個數(shù)據(jù)類型是如何存儲的?
JavaScript 的數(shù)據(jù)類型包括 原始類型 和 引用類型(對象類型) 。
原始類型包括以下 6 個:
StringNumberBooleannullundefinedSymbol
引用類型統(tǒng)稱為 Object 類型,如果細分的話,分為以下 5 個:
ObjectArrayDateRegExpFunction
1、數(shù)據(jù)類型的存儲形式
棧(Stack)和堆(Heap),是兩種基本的數(shù)據(jù)結(jié)構(gòu)。Stack 在內(nèi)存中自動分配內(nèi)存空間的;Heap 在內(nèi)存中動態(tài)分配內(nèi)存空間的,不一定會自動釋放。一般我們在項目中將對象類型手動置為 null 原因,減少無用內(nèi)存消耗。
原始類型(存在棧內(nèi)存中)和對象類型(存在堆內(nèi)存中)分別在內(nèi)存中的存在形式如下圖示:
原始類型是按值形式存放在 棧 中的數(shù)據(jù)段,內(nèi)存空間可以自由分配,同時可以 按值直接訪問。
var a = 10;var b = a;b = 30;console.log(a); // 10值console.log(b); // 30值
過程圖示:
引用類型是存放在 堆 內(nèi)存中,每個對象在堆內(nèi)存中有一個引用地址,就像是每個房間都有一個房間號一樣。引用類型在棧中保存的就是這個對象在堆內(nèi)存的引用地址,我們所說的“房間號”。通過“房間號”可以快速查找到保存在堆內(nèi)存的對象。
var obj1 = new Object();var obj2 = obj1;obj2.name = "小鹿";console.log(obj1.name); // 小鹿
過程圖示:
2、Null
面試官:為什么 typeof null 等于 Object?
不同的對象在底層原理的存儲是用二進制表示的,在 javaScript 中,如果二進制的前三位都為 0 的話,系統(tǒng)會判定為是 Object 類型。 null 的存儲二進制是 000 ,也是前三位,所以系統(tǒng)判定 null 為 Object 類型。
擴展:
這個 bug 個第一版的 javaScript 留下來的。俺也進行擴展一下其他的幾個類型標志位:
3、數(shù)據(jù)類型的判斷
面試官:typeof 與 instanceof 有什么區(qū)別?
typeof 是一元運算符,同樣返回一個字符串類型。一般用來判斷一個變量是否為空或者是什么類型。
除了 null 類型以及 Object 類型不能準確判斷外,其他數(shù)據(jù)類型都可能返回正確的類型。
typeof undefined // 'undefined'typeof '10' // 'String'typeof 10 // 'Number'typeof false // 'Boolean'typeof Symbol() // 'Symbol'typeof Function // ‘function'typeof null // ‘Object’typeof [] // 'Object'typeof {} // 'Object'
既然 typeof 對對象類型都返回 Object 類型情況的局限性,我們可以使用 instanceof 來進行判斷 某個對象是不是另一個對象的實例 。返回值的是一個布爾類型。
var a = [];console.log(a instanceof Array) // true
instanceof 運算符用來測試一個對象在其原型鏈中是否存在一個構(gòu)造函數(shù)的 prototype 屬性,如果對原型鏈不怎能了解,后邊俺會具體的寫到,這里大體記一下就 OK。
我們再測一下 ES6 中的 class 語法糖是什么類型。
class A{}console.log(A instanceof Function) // true
注意:原型鏈中的 prototype 隨時可以被改動的,改變后的值可能不存在于 object 的原型鏈上, instanceof 返回的值可能就返回 false 。
4、類型轉(zhuǎn)換
類型轉(zhuǎn)換通常在面試筆試中出現(xiàn)的比較多,對于類型轉(zhuǎn)換的一些細節(jié)應(yīng)聘者也是很容易忽略的,所以俺整理的盡量系統(tǒng)一些。 javaScript 是一種弱類型語言,變量不受類型限制,所以在特定情況下我們需要對類型進行轉(zhuǎn)換。
「類型轉(zhuǎn)換」分為 顯式類型轉(zhuǎn)換 和 隱式類型轉(zhuǎn)換 。每種轉(zhuǎn)換又分為 原始類型轉(zhuǎn)換 和 對象類型轉(zhuǎn)換 。
顯式類型轉(zhuǎn)換
顯式類型轉(zhuǎn)換就是我們所說強制類型轉(zhuǎn)換。
筆試題:其他數(shù)據(jù)類型轉(zhuǎn)字符串類型!
對于原始類型來說,轉(zhuǎn)字符串類型會默認調(diào)用 toString() 方法。
數(shù)據(jù)類型String類型數(shù)字轉(zhuǎn)化為數(shù)字對應(yīng)的字符串true轉(zhuǎn)化為字符串 "true"null轉(zhuǎn)化為字符串 "null"undefined轉(zhuǎn)化為字符串 “undefined”O(jiān)bject轉(zhuǎn)化為 "[object Object]"
String(123); // "123"String(true); // "true"String(null); // "null"String(undefined);// "undefined"String([1,2,3]) // "1,2,3"String({}); // "[object Object]"
筆試題:其他數(shù)據(jù)類型轉(zhuǎn)布爾類型!
除了特殊的幾個值 ‘’ 、 undefined 、 NAN 、 null 、 false 、 0 轉(zhuǎn)化為 Boolean 為 false 之外,其他類型值都轉(zhuǎn)化為 true 。
Boolean('') // falseBoolean(undefined) // falseBoolean(null) // falseBoolean(NaN) // falseBoolean(false) // falseBoolean(0) // falseBoolean({}) // trueBoolean([]) // true
筆試題:轉(zhuǎn)化為數(shù)字類型!
數(shù)據(jù)類型數(shù)字類型字符串1) 數(shù)字轉(zhuǎn)化為對應(yīng)的數(shù)字
- 其他轉(zhuǎn)化為 NaN布爾類型1) true 轉(zhuǎn)化為 1
- false 轉(zhuǎn)化為 0null0undefinedNaN數(shù)組1) 數(shù)組為空轉(zhuǎn)化為 0;
- 數(shù)組只有一個元素轉(zhuǎn)化為對應(yīng)元素;
- 其他轉(zhuǎn)化為NaN空字符串0
Number(10); // 10 Number('10'); // 10 Number(null); // 0 Number(''); // 0 Number(true); // 1 Number(false); // 0 Number([]); // 0 Number([1,2]); // NaNNumber('10a'); // NaNNumber(undefined); // NaN
筆試題:對象類型轉(zhuǎn)原始類型!
對象類型在轉(zhuǎn)原始類型的時候,會調(diào)用內(nèi)置的 valueOf() 和 toString() 方法,這兩個方法是可以進行重寫的。
轉(zhuǎn)化原始類型分為兩種情況:轉(zhuǎn)化為 字符串類型 或 其他原始類型 。
toString()valueOf()toString()
5、四則運算
隱士類型轉(zhuǎn)化是不需要認為的強制類型轉(zhuǎn)化, javaScript 自動將類型轉(zhuǎn)化為需要的類型,所以稱之為隱式類型轉(zhuǎn)換。
加法運算
加法運算符是在運行時決定,到底是執(zhí)行相加,還是執(zhí)行連接。運算數(shù)的不同,導(dǎo)致了不同的語法行為,這種現(xiàn)象稱為“重載”。
如果雙方都不是字符串,則將轉(zhuǎn)化為數(shù)字或字符串。
- Boolean + Boolean 會轉(zhuǎn)化為數(shù)字相加。
- Boolean + Number 布爾類型轉(zhuǎn)化為數(shù)字相加。
- Object + Number 對象類型調(diào)用 valueOf ,如果不是 String、Boolean 或者 Number類型,則繼續(xù)調(diào)用 toString() 轉(zhuǎn)化為字符串。
true + true // 21 + true // 2[1] + 3 // '13'
字符串和字符串以及字符串和非字符串相加都會進行 連接 。
1 + 'b' // ‘1b’false + 'b' // ‘falseb’
其他運算
其他算術(shù)運算符(比如減法、除法和乘法)都不會發(fā)生重載。它們的規(guī)則是:所有運算子一律轉(zhuǎn)為數(shù)值,再進行相應(yīng)的數(shù)學(xué)運算。
1 * '2' // 21 * [] // 0
6、邏輯運算符
邏輯運算符包括兩種情況,分別為 條件判斷 和 賦值操作 。
條件判斷
&&||
true && true // truetrue && false // falsetrue || true // truetrue || false // true
賦值操作
- A && B
首先看 A 的真假, A 為假,返回 A 的值, A 為真返回 B 的值。(不管 B 是啥)
console.log(0 && 1) // 0console.log(1 && 2) // 2
- A || B
首先看 A 的真假, A 為真返回的是 A 的值, A 為假返回的是 B 的值(不管 B 是啥)
console.log(0 || 1) // 1console.log(1 || 2) // 1
7、比較運算符
比較運算符在邏輯語句中使用,以判定變量或值是否相等。
面試官:== 和 === 的區(qū)別?
對于 === 來說,是嚴格意義上的相等,會比較兩個操作符的類型和值。
- 如果 X 和 Y 的類型不同,返回 false ;
- 如果 X 和 Y 的類型相同,則根據(jù)下方表格進一步判斷
條件例子返回值undefined === undefinedundefined === undefinedtruenull === nullnull === nulltrueString === String
(當字符串順序和字符完全相等的時候返回 true,否則返回 false)‘a(chǎn)’ === 'a'
'a' === 'aa'true
falseBoolean === Booleantrue === true
true === falsetrue
falseSymbol === Symbol相同的 Symbol 返回 true,
不相同的 Symbol 返回 falseNumber === Number
① 其中一個為 NaN,返回 false
② X 和 Y 值相等,返回 true
③ 0 和 -0,返回 true
④ 其他返回 falseNaN ==== NaN
NaN === 1
3 === 3
+0 === -0false
false
true
true
而對于 == 來說,是非嚴格意義上的相等,先判斷兩個操作符的類型是否相等,如果類型不同,則先進行類型轉(zhuǎn)換,然后再判斷值是否相等。
- 如果 X 和 Y 的類型相同,返回 X == Y 的比較結(jié)果;
- 如果 X 和 Y 的類型不同,根據(jù)下方表格進一步判斷;
條件例子返回值null == undefinednull == undefinedtrueString == Number,String 轉(zhuǎn) Number'2' == 2trueBoolean == Number,Boolean 轉(zhuǎn) Numbertrue == 1trueObject == String,Number,Symbol,將 Object 轉(zhuǎn)化為原始類型再比較值大小[1] == 1
[1] == '1'true
true其他返回 falsefalse
this
面試官:什么是 this 指針?以及各種情況下的 this 指向問題。
this 就是一個對象。不同情況下 this 指向的不同,有以下幾種情況,(希望各位親自測試一下,這樣會更容易弄懂):
- 對象調(diào)用, this 指向該對象(前邊誰調(diào)用 this 就指向誰)。
var obj = { name:'小鹿', age: '21', print: function(){ console.log(this) console.log(this.name + ':' + this.age) }}// 通過對象的方式調(diào)用函數(shù)obj.print(); // this 指向 obj
- 直接調(diào)用的函數(shù), this 指向的是全局 window 對象。
function print(){ console.log(this);}// 全局調(diào)用函數(shù)print(); // this 指向 window
- 通過 new 的方式, this 永遠指向新創(chuàng)建的對象。
function Person(name, age){ this.name = name; this.age = age; console.log(this);}var xiaolu = new Person('小鹿',22); // this = > xaiolu
- 箭頭函數(shù)中的 this 。
由于箭頭函數(shù)沒有單獨的 this 值。箭頭函數(shù)的 this 與聲明所在的上下文相同。也就是說調(diào)用箭頭函數(shù)的時候,不會隱士的調(diào)用 this 參數(shù),而是從定義時的函數(shù)繼承上下文。
const obj = { a:()=>{ console.log(this); }}// 對象調(diào)用箭頭函數(shù)
面試官:如何改變 this 的指向?
我們可以通過調(diào)用函數(shù)的 call、apply、bind 來改變 this 的指向。
var obj = { name:'小鹿', age:'22', adress:'小鹿動畫學(xué)編程'}function print(){ console.log(this); // 打印 this 的指向 console.log(arguments); // 打印傳遞的參數(shù)}// 通過 call 改變 this 指向print.call(obj,1,2,3); // 通過 apply 改變 this 指向print.apply(obj,[1,2,3]);// 通過 bind 改變 this 的指向let fn = print.bind(obj,1,2,3);fn();
對于基本的使用想必各位小伙伴都能掌握,俺就不多廢話,再說一說這三者的共同點和不同點。
共同點:
- 三者都能改變 this 指向,且第一個傳遞的參數(shù)都是 this 指向的對象。
- 三者都采用的后續(xù)傳參的形式。
不同點:
- call 的傳參是單個傳遞的(試了下數(shù)組,也是可以的),而 apply 后續(xù)傳遞的參數(shù)是 數(shù)組形式(傳單個值會報錯) ,而 bind 沒有規(guī)定,傳遞值和數(shù)組都可以。
- call 和 apply 函數(shù)的執(zhí)行是直接執(zhí)行的,而 bind 函數(shù)會返回一個函數(shù),然后我們想要調(diào)用的時候才會執(zhí)行。
擴展:如果我們使用上邊的方法改變箭頭函數(shù)的 this 指針,會發(fā)生什么情況呢?能否進行改變呢?
由于箭頭函數(shù)沒有自己的 this 指針,通過 call() 或 apply() 方法調(diào)用一個函數(shù)時,只能傳遞參數(shù)(不能綁定 this ),他們的第一個參數(shù)會被忽略。
new
對于 new 關(guān)鍵字,我們第一想到的就是在面向?qū)ο笾?new 一個實例對象,但是在 JS 中的 new 和 Java 中的 new 的機制不一樣。
一般 Java 中,聲明一個 構(gòu)造函數(shù) ,通過 new 類名() 來創(chuàng)建一個實例,而這個 構(gòu)造函數(shù)是一種特殊的函數(shù)。但是在 JS 中,只要 new 一個函數(shù),就可以 new 一個對象,函數(shù)和構(gòu)造函數(shù)沒有任何的區(qū)別。
面試官:new 內(nèi)部發(fā)生了什么過程?可不可以手寫實現(xiàn)一個 new 操作符?
new 的過程包括以下四個階段:
- 創(chuàng)建一個新對象。
- 這個新對象的 proto 屬性指向原函數(shù)的 prototype 屬性。(即繼承原函數(shù)的原型)
- 將這個新對象綁定到 此函數(shù)的 this 上 。
- 返回新對象,如果這個函數(shù)沒有返回其他對象。
// new 生成對象的過程// 1、生成新對象// 2、鏈接到原型// 3、綁定 this// 4、返回新對象// 參數(shù):// 1、Con: 接收一個構(gòu)造函數(shù)// 2、args:傳入構(gòu)造函數(shù)的參數(shù)function create(Con, ...args){ // 創(chuàng)建空對象 let obj = {}; // 設(shè)置空對象的原型(鏈接對象的原型) obj._proto_ = Con.prototype; // 綁定 this 并執(zhí)行構(gòu)造函數(shù)(為對象設(shè)置屬性) let result = Con.apply(obj,args) // 如果 result 沒有其他選擇的對象,就返回 obj 對象 return result instanceof Object ? result : obj;}// 構(gòu)造函數(shù)function Test(name, age) { this.name = name this.age = age}Test.prototype.sayName = function () { console.log(this.name)}// 實現(xiàn)一個 new 操作符const a = create(Test,'小鹿','23')console.log(a.age)
面試官:有幾種創(chuàng)建對象的方式,字面量相對于 new 創(chuàng)建對象有哪些優(yōu)勢?
最常用的創(chuàng)建對象的兩種方式:
- **new 構(gòu)造函數(shù) **
- 字面量
其他創(chuàng)建對象的方式:
- Object.create()
字面量創(chuàng)建對象的優(yōu)勢所在:
- 代碼量更少,更易讀
- 對象字面量運行速度更快,它們可以在解析的時候被優(yōu)化。他不會像 new 一個對象一樣,解析器需要順著作用域鏈從當前作用域開始查找,如果在當前作用域找到了名為 Object() 的函數(shù)就執(zhí)行,如果沒找到,就繼續(xù)順著作用域鏈往上照,直到找到全局 Object() 構(gòu)造函數(shù)為止。
- Object() 構(gòu)造函數(shù)可以接收參數(shù),通過這個參數(shù)可以把對象實例的創(chuàng)建過程委托給另一個內(nèi)置構(gòu)造函數(shù),并返回另外一個對象實例,而這往往不是你想要的。 對于 Object.create()方式創(chuàng)建對象:
Object.create(proto, [propertiesObject]);
proto:propertiesObject:
一般用于繼承:
var People = function (name){ this.name = name;};People.prototype.sayName = function (){ console.log(this.name);}function Person(name, age){ this.age = age; People.call(this, name); // 使用call,實現(xiàn)了People屬性的繼承};// 使用Object.create()方法,實現(xiàn)People原型方法的繼承,并且修改了constructor指向Person.prototype = Object.create(People.prototype, { constructor: { configurable: true, enumerable: true, value: Person, writable: true }});Person.prototype.sayAge = function (){ console.log(this.age);}var p1 = new Person('person1', 25); p1.sayName(); //'person1'p1.sayAge(); //25
面試官:new/字面量 與 Object.create(null) 創(chuàng)建對象的區(qū)別?
- new 和 字面量創(chuàng)建的對象的原型指向 Object.prototype ,會繼承 Object 的屬性和方法。
- 而通過 Object.create(null) 創(chuàng)建的對象,其原型指向 null , null 作為原型鏈的頂端,沒有也不會繼承任何屬性和方法。
閉包
閉包面試中的重點,但是對于很多初學(xué)者來說都是懵懵的,所以俺就從最基礎(chǔ)的作用域講起,大佬請繞過。
面試官:什么是作用域?什么是作用域鏈?
規(guī)定 變量和函數(shù) 的可使用范圍叫做作用域。只看定義,挺抽象的,舉個例子
function fn1() { let a = 1;}function fn2() { let b = 2;}
聲明兩個函數(shù),分別創(chuàng)建量兩個私有的作用域(可以理解為兩個封閉容器), fn2 是不能直接訪問私有作用域 fn1 的變量 a 的。同樣的,在 fn1 中不能訪問到 fn2 中的 b 變量的。一個函數(shù)就是一個作用域。
每個函數(shù)都會有一個作用域,查找變量或函數(shù)時,由局部作用域到全局作用域依次查找, 這些作用域的集合就稱為作用域鏈。 如果還不是很好理解,俺再舉個例子?:
let a = 1function fn() { function fn1() { function fn2() { let c = 3; console.log(a); } // 執(zhí)行 fn2 fn2(); } // 執(zhí)行 fn1 fn1();}// 執(zhí)行函數(shù)fn();
雖然上邊看起來嵌套有點復(fù)雜,我們前邊說過,一個函數(shù)就是一個私有作用域,根據(jù)定義,在 fn2 作用域中打印 a ,首先在自己所在作用域搜索,如果沒有就向上級作用域搜索,直到搜索到全局作用域, a = 1 ,找到了打印出值。整個搜索的過程,就是基于作用域鏈搜索的。
面試官:什么是閉包?閉包的作用?閉包的應(yīng)用?
很多應(yīng)聘者喜歡這樣回答,“函數(shù)里套一個函數(shù)”,但是面試官更喜歡下面的回答,因為可以繼續(xù)為你挖坑。
函數(shù)執(zhí)行,形成一個私有的作用域,保護里邊的私有變量不受外界的干擾,除了 保護 私有變量外,還可以 保存 一些內(nèi)容,這樣的模式叫做 閉包 。
閉包的作用有兩個, 保護和保存。
保護的應(yīng)用
- 團隊開發(fā)時,每個開發(fā)者把自己的代碼放在一個私有的作用域中,防止相互之間的變量命名沖突;把需要提供給別人的方法,通過 return 或 window.xxx 的方式暴露在全局下。
- jQuery 的源碼中也是利用了這種保護機制。
- 封裝私有變量。
保存的應(yīng)用
- 選項卡閉包的解決方案。
面試官:循環(huán)綁定事件引發(fā)的索引什么問題?怎么解決這種問題?
// 事件綁定引發(fā)的索引問題var btnBox = document.getElementById('btnBox'), inputs = btnBox.getElementsByTagName('input')var len = inputs.length;for(var i = 0; i < 1en; i++){ inputs[i].onclick = function () { alert(i) }}
閉包剩余的部分,俺在之前的文章已經(jīng)總結(jié)過,俺就不復(fù)制過來了,直接傳送過去~動畫:什么是閉包?
原型和原型鏈
面試官:什么是原型?什么是原型鏈?如何理解?
原型:每個 JS 對象都有 proto 屬性,這個屬性指向了原型。跟俺去看看,
再來一個,
我們可以看到,只要是對象類型,都會有這個 proto 屬性,這個屬性指向的也是一個原型對象,原型對象也是對象呀,肯定也會存在一個 proto 屬性。那么就形成了原型鏈,定義如下:
原型鏈:原型鏈就是多個對象通過 proto 的方式連接了起來。
原型和原型鏈是怎么來的呢?如果理清原型鏈中的關(guān)系呢?
對于原型和原型鏈的前世今生,由于篇幅過大,俺的傳送門~
圖解:告訴面試官什么是 JS 原型和原型鏈?
PS:下面的看不懂,一定去看文章哦!
再往深處看,他們之間存在復(fù)雜的關(guān)系,但是這些所謂的負責(zé)關(guān)系俺已經(jīng)總結(jié)好了,小二上菜
這張圖看起來真復(fù)雜,但是通過下邊總結(jié)的,再來分析這張圖,試試看。
- 所有的實例的 proto 都指向該構(gòu)造函數(shù)的原型對象( prototype )。
- 所有的函數(shù)(包括構(gòu)造函數(shù))是 Function() 的實例,所以所有函數(shù)的 proto 的都指向 Function() 的原型對象。
- 所有的原型對象(包括 Function 的原型對象)都是 Object 的實例,所以 proto 都指向 Object (構(gòu)造函數(shù))的原型對象。而 Object 構(gòu)造函數(shù)的 proto 指向 null 。
- Function 構(gòu)造函數(shù)本身就是 Function 的實例,所以 proto 指向 Function 的原型對象。
面試官:instanceOf 的原理是什么?
之前留了一個小問題,總結(jié)了上述的原型和原型鏈之后, instanceof 的原理很容易理解。
instanceof 的原理是通過判斷該對象的原型鏈中是否可以找到該構(gòu)造類型的 prototype 類型。
function Foo(){}var f1 = new Foo();console.log(f1 instanceof Foo);// true
繼承
面試官:說一說 JS 中的繼承方式有哪些?以及各個繼承方式的優(yōu)缺點。
經(jīng)典繼承(構(gòu)造函數(shù))
/ 詳細解析//1、當用調(diào)用 call 方法時,this 帶邊 son 。//2、此時 Father 構(gòu)造函數(shù)中的 this 指向 son。//3、也就是說 son 有了 colors 的屬性。//4、每 new 一個 son ,都會產(chǎn)生不同的對象,每個對象的屬性都是相互獨立的。function Father(){ this.colors = ["red","blue","green"];}function Son(){ // this 是通過 new 操作內(nèi)部的新對象 {} , // 此時 Father 中的 this 就是為 Son 中的新對象{} // 新對象就有了新的屬性,并返回得到 new 的新對象實例 // 繼承了Father,且向父類型傳遞參數(shù) Father.call(this);}let s = new Son();console.log(s.color)
① 基本思想:在子類的構(gòu)造函數(shù)的內(nèi)部調(diào)用父類的構(gòu)造函數(shù)。
② 優(yōu)點:
- 保證了原型鏈中引用類型的獨立,不被所有實例共享。
- 子類創(chuàng)建的時候可以向父類進行傳參。
③ 缺點:
- 繼承的方法都在構(gòu)造函數(shù)中定義,構(gòu)造函數(shù)不能夠復(fù)用了(因為構(gòu)造函數(shù)中存在子類的特殊屬性,所以構(gòu)造函數(shù)中復(fù)用的屬性不能復(fù)用了)。
- 父類中定義的方法對于子類型而言是不可見的(子類所有的屬性都定義在父類的構(gòu)造函數(shù)當中)。
組合繼承
function Father(name){ this.name = name; this.colors = ["red","blue","green"];}// 方法定義在原型對象上(共享)Father.prototype.sayName = function(){ alert(this.name);};function Son(name,age){ // 子類繼承父類的屬性 Father.call(this,name); //繼承實例屬性,第一次調(diào)用 Father() // 每個實例都有自己的屬性 this.age = age;}// 子類和父類共享的方法(實現(xiàn)了父類屬性和方法的復(fù)用) Son.prototype = new Father(); //繼承父類方法,第二次調(diào)用 Father()// 子類實例對象共享的方法Son.prototype.sayAge = function(){ alert(this.age);}var instance1 = new Son("louis",5);instance1.colors.push("black");console.log(instance1.colors);//"red,blue,green,black"instance1.sayName();//louisinstance1.sayAge();//5var instance1 = new Son("zhai",10);console.log(instance1.colors);//"red,blue,green"instance1.sayName();//zhaiinstance1.sayAge();//10
① 基本思想:
- 使用 原型鏈 實現(xiàn)對「原型對象屬性和方法」的繼承。
- 通過借用 構(gòu)造函數(shù) 來實現(xiàn)對「實例屬性」的繼承。
② 優(yōu)點:
- 在原型對象上定義的方法實現(xiàn)了函數(shù)的復(fù)用。
- 每個實例都有屬于自己的屬性。
③ 缺點:
- 組合繼承調(diào)用了兩次父類的構(gòu)造函數(shù),造成了不必要的消耗。
原型繼承
function object(o){ function F(){} F.prototype = o; // 每次返回的 new 是不同的 return new F();}var person = { friends : ["Van","Louis","Nick"]};// 實例 1var anotherPerson = object(person);anotherPerson.friends.push("Rob");// 實例 2var yetAnotherPerson = object(person);yetAnotherPerson.friends.push("Style");// 都添加至原型對象的屬性(所共享)alert(person.friends); // "Van,Louis,Nick,Rob,Style"
① 基本思想:創(chuàng)建臨時性的構(gòu)造函數(shù)(無任何屬性),將傳入的對象作為該構(gòu)造函數(shù)的原型對象,然后返回這個新構(gòu)造函數(shù)的實例。
② 淺拷貝:
object 所產(chǎn)生的對象是不相同的,但是原型對象都是 person 對象,所改變存在原型對象的屬性所有生成的實例所共享,不僅被 Person 所擁有,而且被子類生成的實例所共享。
③ object.create():在 ECMAScript5 中,通過新增 object.create() 方法規(guī)范化了上面的原型式繼承.。
- 參數(shù)一:新對象的原型的對象。
- 參數(shù)二:先對象定義額外的屬性(可選)。
寄生式繼承
function createAnother(original){ var clone = object(original); // 通過調(diào)用object函數(shù)創(chuàng)建一個新對象 clone.sayHi = function(){ // 以某種方式來增強這個對象 alert("hi"); }; return clone; //返回這個對象}
- 基本思想:不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù)(避免第二次調(diào)用的構(gòu)造函數(shù))。
- 優(yōu)點:寄生組合式繼承就是為了解決組合繼承中兩次調(diào)用構(gòu)造函數(shù)的開銷。
垃圾回收機制
說到 Javascript 的垃圾回收機制,我們要從內(nèi)存泄漏一步步說起。
面試官:什么是內(nèi)存泄漏?為什么會導(dǎo)致內(nèi)存泄漏?
不再用到的內(nèi)存,沒有及時釋放,就叫做內(nèi)存泄漏。
內(nèi)存泄漏是指我們已經(jīng)無法再通過js代碼來引用到某個對象,但垃圾回收器卻認為這個對象還在被引用,因此在回收的時候不會釋放它。導(dǎo)致了分配的這塊內(nèi)存永遠也無法被釋放出來。如果這樣的情況越來越多,會導(dǎo)致內(nèi)存不夠用而系統(tǒng)崩潰。
面試官:怎么解決內(nèi)存泄漏?說一說 JS 垃圾回收機制的運行機制的原理?
很多編程語言需要手動釋放內(nèi)存,但是很多開發(fā)者喜歡系統(tǒng)提供自動內(nèi)存管理,減輕程序員的負擔(dān),這被稱為 "垃圾回收機制" 。
之所以會有垃圾回收機制,是因為 js 中的字符串、對象、數(shù)組等只有確定固定大小時,才會動態(tài)分配內(nèi)存,只要像這樣動態(tài)地分配了內(nèi)存,最終都要釋放這些內(nèi)存以便他們能夠被再用,否則, JavaScript 的解釋器將會消耗完系統(tǒng)中所有可用的內(nèi)存,造成系統(tǒng)崩潰
JavaScript 與其他語言不同,它具有自動垃圾收集機制,執(zhí)行環(huán)境會負責(zé)管理代碼執(zhí)行過程中使用的內(nèi)存。
兩種垃圾回收策略
找出那些不再繼續(xù)使用的變量,然后釋放其內(nèi)存。垃圾回收器會按照固定的時間間隔,周期性的執(zhí)行該垃圾回收操作。
共有兩種策略:
- 標記清除法
- 引用計數(shù)法
標記清除法
垃圾回收器會在運行的時候,會給存儲在內(nèi)存中的所有變量都加上標記,然后它會去掉環(huán)境中變量以及被環(huán)境中的變量引用的變量的標記。剩下的就視為即將要刪除的變量,原因是在環(huán)境中無法訪問到這些變量了。最后垃圾回收器完成內(nèi)存清除操作。
它的實現(xiàn)原理就是通過判斷一個變量是否在執(zhí)行環(huán)境中被引用,來進行標記刪除。
引用計數(shù)法
引用計數(shù)的垃圾收集策略不常用,引用計數(shù)的最基本含義就是跟蹤記錄每個值被引用的次數(shù)。
當聲明變量并將一個引用類型的值賦值給該變量時,則這個值的引用次數(shù)加 1,同一值被賦予另一個變量,該值的引用計數(shù)加 1 。當引用該值的變量被另一個值所取代,則引用計數(shù)減 1,當計數(shù)為 0 的時候,說明無法在訪問這個值了,所有系統(tǒng)將會收回該值所占用的內(nèi)存空間。
存在的缺陷:
兩個對象的相互循環(huán)引用,在函數(shù)執(zhí)行完成的時候,兩個對象相互的引用計數(shù)并未歸 0 ,而是依然占據(jù)內(nèi)存,無法回收,當該函數(shù)執(zhí)行多次時,內(nèi)存占用就會變多,導(dǎo)致大量的內(nèi)存得不到回收。
最常見的就是在 IE BOM 和 DOM 中,使用的對象并不是 js 對象,所以垃圾回收是基于計數(shù)策略的。但是在 IE9 已經(jīng)將 BOM 和 DOM 真正的轉(zhuǎn)化為了 js 對象,所以循環(huán)引用的問題得到解決。
如何管理內(nèi)存
雖然說是 js 的內(nèi)存都是自動管理的,但是對于 js 還是存在一些問題的,最主要的一個問題就是 分配給 Web 瀏覽器的可用內(nèi)存數(shù)量通常比分配給桌面應(yīng)用程序的少 。
為了能夠讓頁面獲得最好的性能,必須確保 js 變量占用最少的內(nèi)存,最好的方式就是將不用的變量引用釋放掉,也叫做 解除引用 。
- 對于局部變量來說,函數(shù)執(zhí)行完成離開環(huán)境變量,變量將自動解除。
- 對于全局變量我們需要進行手動解除。(注意:解除引用并不意味被收回,而是將變量真正的脫離執(zhí)行環(huán)境,下一次垃圾回收將其收回)
var a = 20; // 在堆內(nèi)存中給數(shù)值變量分配空間alert(a + 100); // 使用內(nèi)存var a = null; // 使用完畢之后,釋放內(nèi)存空間
補充:因為通過上邊的垃圾回收機制的標記清除法的原理得知,只有與環(huán)境變量失去引用的變量才會被標記回收,所用上述例子通過將對象的引用設(shè)置為 null ,此變量也就失去了引用,等待被垃圾回收器回收。
深拷貝和淺拷貝
面試官:什么是深拷貝?什么是淺拷貝?
上邊在 JavaScript 基本類型中我們說到,數(shù)據(jù)類型分為 基本類型和引用類型 。對基本類型的拷貝就是對值復(fù)制進行一次拷貝,而對于引用類型來說,拷貝的不是值,而是 值的地址 ,最終兩個變量的地址指向的是同一個值。還是以前的例子:
var a = 10;var b = a;b = 30;console.log(a); // 10值console.log(b); // 30值var obj1 = new Object();var obj2 = obj1;obj2.name = "小鹿";console.log(obj1.name); // 小鹿
要想將 obj1 和 obj2 的關(guān)系斷開,也就是不讓他指向同一個地址。根據(jù)不同層次的拷貝,分為深拷貝和淺拷貝。
- 淺拷貝: 只進行一層關(guān)系的拷貝。
- 深拷貝: 進行無限層次的拷貝。
面試官:淺拷貝和深拷貝分別如何實現(xiàn)的?有哪幾種實現(xiàn)方式?
- 自己實現(xiàn)一個淺拷貝:
// 實現(xiàn)淺克隆function shallowClone(o){ const obj = {}; for(let i in o){ obj[i] = o[i] } return obj;}
- 擴展運算符實現(xiàn):
let a = {c: 1}let b = {...a}a.c = 2console.log(b.c) // 1
- Object.assign() 實現(xiàn)
let a = {c: 1}let b = Object.assign({}, a)a.c = 2console.log(b.c) // 1
對于深拷貝來說,在淺拷貝的基礎(chǔ)上加上遞歸,我們改動上邊自己實現(xiàn)的淺拷貝代碼:
var a1 = {b: {c: {d: 1}};function clone(source) { var target = {}; for(var i in source) { if (source.hasOwnProperty(i)) { if (typeof source[i] === 'object') { target[i] = clone(source[i]); // 遞歸 } else { target[i] = source[i]; } } } return target;}
如果功底稍微扎實的小伙伴可以看出上邊深拷貝存在的問題:
- 參數(shù)沒有做檢驗;
- 判斷對象不夠嚴謹;
- 沒有考慮到數(shù)組,以及 ES6 的 set, map, weakset, weakmap 兼容性。
- 最嚴重的問題就是遞歸容易爆棧(遞歸層次很深的時候)。
- 循環(huán)引用問題提。
var a = {};a.a = a;clone(a); // 會造成一個死循環(huán)
兩種解決循環(huán)引用問題的辦法:
- 暴力破解
- 循環(huán)檢測
還有一個最簡單的實現(xiàn)深拷貝的方式,那就是利用 JSON.parse(JSON.stringify(object)) ,但是也存在一定的局限性。
function cloneJSON(source) { return JSON.parse(JSON.stringify(source));}
對于這種方法來說,內(nèi)部的原理實現(xiàn)也是使用的遞歸,遞歸到一定深度,也會出現(xiàn)爆棧問題。但是對于循環(huán)引用的問題不會出現(xiàn),內(nèi)部的解決方案正是用到了循環(huán)檢測。對于詳細的實現(xiàn)一個深拷貝,具體參考文章: 深拷貝的終極探索
異步編程
由于 JavaScript 是單線程的,單線程就意味著阻塞問題,當一個任務(wù)執(zhí)行完成之后才能執(zhí)行下一個任務(wù)。這樣就會導(dǎo)致出現(xiàn)頁面卡死的狀態(tài),頁面無響應(yīng),影響用戶的體驗,所以不得不出現(xiàn)了同步和異步的解決方案。
面試官:JS 為什么是單線程?又帶來了哪些問題呢?
JS 單線程的特點就是同一時刻只能執(zhí)行一個任。這是由一些與用戶的互動以及操作 DOM 等相關(guān)的操作決定了 JS 要使用單線程,否則使用多線程會帶來復(fù)雜的同步問題。如果執(zhí)行同步問題的話,多線程需要加鎖,執(zhí)行任務(wù)造成非常的繁瑣。
雖然 HTML5 標準規(guī)定,允許 JavaScript 腳本創(chuàng)建多個線程,但是子線程完全受主線程控制,且不得操作 DOM 。
上述開頭我們也說到了,單線程帶來的問題就是會導(dǎo)致阻塞問題,為了解決這個問題,就不得不涉及 JS 的兩種任務(wù),分別為同步任務(wù)和異步任務(wù)。
面試官:JS 如何實現(xiàn)異步編程?
最早的解決方案是使用回調(diào)函數(shù),回調(diào)函數(shù)不是直接調(diào)用,而是在特定的事件或條件發(fā)生時另一方調(diào)用的,用于對該事件或條件進行響應(yīng)。比如 Ajax 回調(diào):
// jQuery 中的 ajax$.ajax({ type : "post", url : 'test.json', dataType : 'json', success : function(res) { // 響應(yīng)成功回調(diào) }, fail: function(err){ // 響應(yīng)失敗回調(diào) }});
但是如果某個請求存在依賴性,如下:
$.ajax({ type:"post", success: function(res){//成功回調(diào) //再次異步請求 $.ajax({ type:"post", url:"...?id=res.id, success:function(res){ $.ajax({ type:"post", url:"...?id=res.id, success:function(){ // 往復(fù)循環(huán) } }) } }) }})
就會形成不斷的循環(huán)嵌套,我們稱之為回調(diào)地獄。我們可以看出回調(diào)地獄有以下缺點:
try catchreturn
以上有兩個地方俺需要再進一步詳細說明一下:
- 為什么不能捕獲異常?
其實這跟 js 的運行機制相關(guān),異步任務(wù)執(zhí)行完成會加入任務(wù)隊列,當執(zhí)行棧中沒有可執(zhí)行任務(wù)了,主線程取出任務(wù)隊列中的異步任務(wù)并入棧執(zhí)行,當異步任務(wù)執(zhí)行的時候,捕獲異常的函數(shù)已經(jīng)在執(zhí)行棧內(nèi)退出了,所以異常無法被捕獲。
- 為什么不能return?
return 只能終止回調(diào)的函數(shù)的執(zhí)行,而不能終止外部代碼的執(zhí)行。
面試官:如何解決回調(diào)地獄問題呢?
既然回調(diào)函數(shù)存在回調(diào)地獄問題,那我們?nèi)绾谓鉀Q呢?ES6 給我們提供了三種解決方案,分別是 Generator、Promise、async/await(ES7)。
由于這部分涉及到 ES6 部分的知識,這一期是有關(guān) JS 的,所以會在下一期進行延伸,這里不多涉及。
【留下一個傳送門~】
面試官:說說異步代碼的執(zhí)行順序?Event Loop 的運行機制是如何的運行的?
上邊我們說到 JS 是單線程且使用同步和異步任務(wù)解決 JS 的阻塞問題,那么異步代碼的執(zhí)行順序以及 EventLoop 是如何運作的呢?
在深入事件循環(huán)機制之前,需要弄懂一下幾個概念:
- 執(zhí)行上下文 ( Execution context )
- 執(zhí)行棧 ( Execution stack )
- 微任務(wù) ( micro-task )
- 宏任務(wù) ( macro-task )
執(zhí)行上下文
執(zhí)行上下文是一個抽象的概念,可以理解為是代碼執(zhí)行的一個環(huán)境。JS 的執(zhí)行上下文分為三種, 全局執(zhí)行上下文、函數(shù)(局部)執(zhí)行上下文、Eval 執(zhí)行上下文 。
- 全局執(zhí)行上下文 —— 全局執(zhí)行上下文指的是全局 this 指向的 window ,可以是外部加載的 JS 文件或者本地 <scripe></script> 標簽中的代碼。
- 函數(shù)執(zhí)行上下文 —— 函數(shù)上下文也稱為局部上下文,每個函數(shù)被調(diào)用的時候,都會創(chuàng)建一個新的局部上下文。
- Eval 執(zhí)行上下文 —— 這個不經(jīng)常用,所以不多討論。
執(zhí)行棧
執(zhí)行棧,就是我們數(shù)據(jù)結(jié)構(gòu)中的“棧”,它具有“先進后出”的特點,正是因為這種特點,在我們代碼進行執(zhí)行的時候,遇到一個執(zhí)行上下文就將其依次壓入執(zhí)行棧中。
當代碼執(zhí)行的時候,先執(zhí)行位于棧頂?shù)膱?zhí)行上下文中的代碼,當棧頂?shù)膱?zhí)行上下文代碼執(zhí)行完畢就會出棧,繼續(xù)執(zhí)行下一個位于棧頂?shù)膱?zhí)行上下文。
function foo() { console.log('a'); bar(); console.log('b');}function bar() { console.log('c');}foo();
- 初始化狀態(tài),執(zhí)行棧任務(wù)為空。
- foo 函數(shù)執(zhí)行,foo 進入執(zhí)行棧,輸出 a,碰到函數(shù) bar。
- 然后 bar 再進入執(zhí)行棧,開始執(zhí)行 bar 函數(shù),輸出 c。
- bar 函數(shù)執(zhí)行完出棧,繼續(xù)執(zhí)行執(zhí)行棧頂端的函數(shù) foo,最后輸出 c。
- foo 出棧,所有執(zhí)行棧內(nèi)任務(wù)執(zhí)行完畢。
宏任務(wù)
對于宏任務(wù)一般包括:
scriptsetTimeoutsetIntervalsetImmediateI/O
微任務(wù)
對于微任務(wù)一般包括:
Promiseprocess.nextTickMutationObserver
注意:nextTick 隊列會比 Promie 隊列先執(zhí)行。
運行機制
以上概念弄明白之后,再來看循環(huán)機制是如何運行的呢?以下涉及到的任務(wù)執(zhí)行順序都是靠函數(shù)調(diào)用棧來實現(xiàn)的。
1)首先,事件循環(huán)機制的是從 <script> 標簽內(nèi)的代碼開始的,上邊我們提到過,整個 script 標簽作為一個宏任務(wù)處理的。
2)在代碼執(zhí)行的過程中,如果遇到宏任務(wù),如: setTimeout ,就會將當前任務(wù)分發(fā)到對應(yīng)的執(zhí)行隊列中去。
3)當執(zhí)行過程中,如果遇到微任務(wù),如: Pomise ,在創(chuàng)建 Promise 實例對象時,代碼順序執(zhí)行,如果到了執(zhí)行· then 操作,該任務(wù)就會被分發(fā)到微任務(wù)隊列中去。
4) script 標簽內(nèi)的代碼執(zhí)行完畢,同時執(zhí)行過程中所涉及到的宏任務(wù)也和微任務(wù)也分配到相應(yīng)的隊列中去。
5)此時宏任務(wù)執(zhí)行完畢,然后去微任務(wù)隊列執(zhí)行所有的存在的微任務(wù)。
6)微任務(wù)執(zhí)行完畢,第一輪的消息循環(huán)執(zhí)行完畢,頁面進行一次渲染。
7)然后開始第二輪的消息循環(huán),從宏任務(wù)隊列中取出任務(wù)執(zhí)行。
8)如果兩個任務(wù)隊列沒有任務(wù)可執(zhí)行了,此時所有任務(wù)執(zhí)行完畢。
實戰(zhàn)一下:(動畫演示)
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>消息運行機制</title></head><body></body> <script> console.log('1'); setTimeout(() => { console.log('2') }, 1000); new Promise((resolve, reject) => { console.log('3'); resolve(); console.log('4'); }).then(() => { console.log('5'); }); console.log('6');// 1,3,4,6,5,2 </script></html>
- 初始化狀態(tài),執(zhí)行棧為空。
- 首先執(zhí)行 <script> 標簽內(nèi)的同步代碼,此時全局的代碼進入執(zhí)行棧中,同步順序執(zhí)行代碼,輸出 1。
- 執(zhí)行過程中遇到異步代碼 setTimeout (宏任務(wù)),將其分配到宏任務(wù)異步隊列中。
- 同步代碼繼續(xù)執(zhí)行,遇到一個 promise 異步代碼(微任務(wù))。但是構(gòu)造函數(shù)中的代碼為同步代碼,依次輸出3、4,則 then 之后的任務(wù)加入到微任務(wù)隊列中去。
- 最后執(zhí)行同步代碼,輸出 6。
- 因為 script 內(nèi)的代碼作為宏任務(wù)處理,所以此次循環(huán)進行到處理微任務(wù)隊列中的所有異步任務(wù),直達微任務(wù)隊列中的所有任務(wù)執(zhí)行完成為止,微任務(wù)隊列中只有一個微任務(wù),所以輸出 5。
- 此時頁面要進行一次頁面渲染,渲染完成之后,進行下一次循環(huán)。
- 在宏任務(wù)隊列中取出一個宏任務(wù),也就是之前的 setTimeout ,最后輸出 2。
- 此時任務(wù)隊列為空,執(zhí)行棧中為空,整個程序執(zhí)行完畢。
以上難免有些啰嗦,所以簡化整理如下步驟:
- 一開始執(zhí)行宏任務(wù)(script 中同步代碼),執(zhí)行完畢,調(diào)用棧為空。
- 然后檢查微任務(wù)隊列是否有可執(zhí)行任務(wù),執(zhí)行完所有微任務(wù)。
- 進行頁面渲染。
- 第二輪從宏任務(wù)隊列取出一個宏任務(wù)執(zhí)行,重復(fù)以上循環(huán)。