動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

面試官:說說 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)存中的存在形式如下圖示:

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

原始類型是按值形式存放在 中的數(shù)據(jù)段,內(nèi)存空間可以自由分配,同時可以 按值直接訪問

var a = 10;var b = a;b = 30;console.log(a); // 10值console.log(b); // 30值

過程圖示:

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

引用類型是存放在 內(nèi)存中,每個對象在堆內(nèi)存中有一個引用地址,就像是每個房間都有一個房間號一樣。引用類型在棧中保存的就是這個對象在堆內(nèi)存的引用地址,我們所說的“房間號”。通過“房間號”可以快速查找到保存在堆內(nèi)存的對象。

var obj1 = new Object();var obj2 = obj1;obj2.name = "小鹿";console.log(obj1.name); // 小鹿

過程圖示:

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

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ù)字

  1. 其他轉(zhuǎn)化為 NaN布爾類型1) true 轉(zhuǎn)化為 1
  2. false 轉(zhuǎn)化為 0null0undefinedNaN數(shù)組1) 數(shù)組為空轉(zhuǎn)化為 0;
  3. 數(shù)組只有一個元素轉(zhuǎn)化為對應(yīng)元素;
  4. 其他轉(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ù)就是一個作用域。

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

每個函數(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 ,找到了打印出值。整個搜索的過程,就是基于作用域鏈搜索的。

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

面試官:什么是閉包?閉包的作用?閉包的應(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 屬性,這個屬性指向了原型。跟俺去看看,

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

再來一個,

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

我們可以看到,只要是對象類型,都會有這個 proto 屬性,這個屬性指向的也是一個原型對象,原型對象也是對象呀,肯定也會存在一個 proto 屬性。那么就形成了原型鏈,定義如下:

原型鏈:原型鏈就是多個對象通過 proto 的方式連接了起來。

原型和原型鏈是怎么來的呢?如果理清原型鏈中的關(guān)系呢?

對于原型和原型鏈的前世今生,由于篇幅過大,俺的傳送門~

圖解:告訴面試官什么是 JS 原型和原型鏈?

PS:下面的看不懂,一定去看文章哦!

再往深處看,他們之間存在復(fù)雜的關(guān)系,但是這些所謂的負責(zé)關(guān)系俺已經(jīng)總結(jié)好了,小二上菜

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

這張圖看起來真復(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
動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

繼承

面試官:說一說 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í)行完畢。
動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

宏任務(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í)行完畢。
動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

以上難免有些啰嗦,所以簡化整理如下步驟:

  • 一開始執(zhí)行宏任務(wù)(script 中同步代碼),執(zhí)行完畢,調(diào)用棧為空。
  • 然后檢查微任務(wù)隊列是否有可執(zhí)行任務(wù),執(zhí)行完所有微任務(wù)。
  • 進行頁面渲染。
  • 第二輪從宏任務(wù)隊列取出一個宏任務(wù)執(zhí)行,重復(fù)以上循環(huán)。

來源:https://www.tuicool.com/articles/QRBzAvz

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容