掘金出的小冊《前端面試之道》是本好書,但可能由於本人基礎太差的原因一開始看不太懂,最近在找實習不得不硬啃,所以基於小冊找了點資料查漏補缺。
內建型別
JS 中分為七種內建型別,七種內建型別又分為兩大型別:基本型別和物件(Object)。
基本型別有六種: null,undefined,boolean,number,string,symbol(ES6新增)
關於Symbol:表示獨一無二的值,從根本上防止屬性名的衝突。是一種類似於字串的資料型別.
棧:原始資料型別(Undefined,Null,Boolean,Number、String) 堆:引用資料型別(物件、陣列和函式(特殊物件)) 引用資料型別在棧中儲存了指標,該指標指向堆中該實體的起始地址。當直譯器尋找引用值時,會首先檢索其在棧中的地址,取得地址後從堆中獲得實體。
let s = Symbol();
typeof s // "symbol"
複製程式碼
NaN 也屬於 number 型別,並且 NaN 不等於自身。
建立物件
- 物件字面量
- 用function來模擬無參的建構函式
- 用function來模擬參建構函式來實現(用this關鍵字定義構造的上下文屬性)
- 用工廠方式來建立(內建物件)
- 用原型方式來建立
Typeof
typeof 對於基本型別,除了 null 都可以顯示正確的型別
typeof 對於物件,除了函式都會顯示 object
對於 null 來說,雖然它是基本型別,但是會顯示 object,這是一個存在很久了的 Bug
如果我們想獲得一個變數的正確型別,可以通過
Object.prototype.toString.call(xx)。
型別轉換
- 轉Boolean:
在條件判斷時,除了 undefined, null, false, NaN, '', 0, -0 ,其他所有值都轉為 true,包括所有物件。
- 物件轉基本型別:
首先會呼叫valueOf
然後呼叫 toString
。
(或者重寫 Symbol.toPrimitive ,該方法在轉基本型別時呼叫優先順序最高。)
字串轉數字:parseFloat('12.3b');
- 四則運算子
只有當加法運算時,其中一方是字串型別,就會把另一個也轉為字串型別。其他運算只要其中一方是數字,那麼另一方就轉為數字。
加法運算會觸發三種型別轉換: 值轉換為原始值,轉換為數字,轉換為字串。
- == 操作符
null == undefined //true
解析[] == ![] // -> true
// [] 轉成 true,然後取反變成 false
[] == false
// 根據第 8 條得出
// 若Type(y) is boolean, compare x == ToNumber(y)
[] == ToNumber(false)
[] == 0
// 根據第 10 條得出
// if Type(x) is object and Type(y) is string or number, compare ToPrimitive(x) == y
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根據第 6 條得出
0 == 0 // -> true
複製程式碼
- 比較運算子
- 如果是物件,就通過 toPrimitive 轉換物件
- 如果是字串,就通過 unicode 字元索引來比較
原型
每個函式都有 prototype 屬性,除了 Function.prototype.bind()
,該屬性指向原型。
每個物件都有__proto__
屬性,指向了建立該物件的建構函式的原型。
其實這個屬性指向了 [[prototype]]
,但是 [[prototype]]
是內部屬性,我們並不能訪問到,所以使用__proto__
來訪問。
物件可以通過__proto__
來尋找不屬於該物件的屬性, 因為__proto__
將物件連線起來組成了原型鏈。
總結:
Object
是所有物件的爸爸,所有物件都可以通過__proto__
找到它Function
是所有函式的爸爸,所有函式都可以通過__proto__
找到它Function.prototype
和Object.prototype
是兩個特殊的物件,他們由引擎來建立- 除了以上兩個特殊物件,其他物件都是通過構造器
new
出來的 - 函式的
prototype
是一個物件,也就是原型 - 物件的
__proto__
指向原型,__proto__
將物件和原型連線起來組成了原型鏈
原型鏈例子:
function Father(){
this.property = true;
}
Father.prototype.getFatherValue = function(){
return this.property;
}
function Son(){
this.sonProperty = false;
}
//繼承 Father
Son.prototype = new Father();
//Son.prototype被重寫,導致Son.prototype.constructor也一同被重寫
Son.prototype.getSonVaule = function(){
return this.sonProperty;
}
var instance = new Son();
alert(instance.getFatherValue());//true
//instance例項通過原型鏈找到了Father原型中的getFatherValue方法.
複製程式碼
原型鏈存在的問題:
- 當原型鏈中包含引用型別值的原型時,該引用型別值會被所有例項共享;
- 在建立子型別(例如建立Son的例項)時,不能向超型別(例如Father)的建構函式中傳遞引數.
建議的繼承方式:
- 使用借用建構函式+原型鏈 = 組合繼承混合方式。
在子類建構函式內部使用apply或者call來呼叫父類的函式即可在實現屬性繼承的同時,又能傳遞引數,又能讓例項不互相影響
function Super(){
this.flag = true;
}
Super.prototype.getFlag = function(){
return this.flag; //繼承方法
}
function Sub(){
this.subFlag = flase
Super.call(this) //繼承屬性
}
Sub.prototype = new Super;
Sub.prototype.constructor = Sub;
var obj = new Sub();
Super.prototype.getSubFlag = function(){
return this.flag;
}
複製程式碼
小問題:Sub.prototype = new Super; 會導致Sub.prototype的constructor指向Super;
然而constructor的定義是要指向原型屬性對應的建構函式的,Sub.prototype是Sub建構函式的原型,所以應該新增一句糾正:Sub.prototype.constructor = Sub;
組合繼承是 JavaScript 最常用的繼承模式; 不過, 它也有自己的不足。 組合繼承最大的問題就是無論什麼情況下, 都會呼叫兩次父類建構函式: 一次是在建立子型別原型的時候, 另一次是在子型別建構函式內部。
- 寄生組合式繼承 就是為了降低呼叫父類建構函式的開銷而出現的。
基本思路是: 不必為了指定子型別的原型而呼叫超型別的建構函式。
function extend(subClass,superClass){
var prototype = object(superClass.prototype);//建立物件
prototype.constructor = subClass;//增強物件
subClass.prototype = prototype;//指定物件
}
複製程式碼
extend的高效率體現在它沒有呼叫superClass建構函式,因此避免了在subClass.prototype上面建立不必要,多餘的屬性. 於此同時,原型鏈還能保持不變; 因此還能正常使用 instanceof 和 isPrototypeOf() 方法.
- ES6的class 其內部其實也是ES5組合繼承的方式,通過call借用建構函式,在A類建構函式中呼叫相關屬性,再用原型鏈的連線實現方法的繼承
class B extends A {
constructor() {
return A.call(this); //繼承屬性
}
}
A.prototype = new B; //繼承方法
複製程式碼
ES6封裝了class,extends關鍵字來實現繼承,內部的實現原理其實依然是基於上面所講的原型鏈,不過進過一層封裝後,Javascript的繼承得以更加簡潔優雅地實現
class ColorPoint extends Point {
//通過constructor來定義建構函式,用super呼叫父類的屬性方法
constructor(x, y, color) {
super(x, y); // 等同於parent.constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 等同於parent.toString()
}
}
複製程式碼
new
呼叫new的過程會發生:
- 新生成了一個空物件,並且 this 變數引用該物件,同時還繼承了該函式的原型。
- 加入屬性和方法。
- 返回新物件。
//自定義new
function create() {
// 建立一個空的物件
let obj = new Object()
// 獲得建構函式
let Con = [].shift.call(arguments)
// 連結到原型
obj.__proto__ = Con.prototype
// 繫結 this,執行建構函式
let result = Con.apply(obj, arguments)
// 確保 new 出來的是個物件
return typeof result === 'object' ? result : obj
}
複製程式碼
對於建立一個物件來說,更推薦使用字面量的方式建立物件
(無論效能上還是可讀性)。
因為使用 new Object() 的方式建立物件需要通過作用域鏈一層層找到 Object,但是你使用字面量的方式就沒這個問題。
function Foo() {}
// function 就是個語法糖
// 內部等同於 new Function()
let a = { b: 1 }
// 這個字面量內部也是使用了 new Object()
複製程式碼
new Foo() 的優先順序大於 new Foo
new Foo.getName(); // -> new (Foo.getName());
new Foo().getName();
// -> (new Foo()).getName();
//先執行 new Foo() 產生了一個例項,然後通過原型鏈找到了 Foo 上的 getName 函式
複製程式碼
instanceof
instanceof 可以正確的判斷物件的型別,因為內部機制是通過判斷物件的原型鏈中是不是能找到型別的 prototype。
this
補充:普通函式和建構函式的區別
- 建構函式也是一個普通函式,建立方式一樣,但建構函式習慣上首字母大寫
- 建構函式和普通函式的區別在於:呼叫方式不一樣 普通函式的呼叫方式:直接呼叫 person(); 建構函式的呼叫方式:需要使用new關鍵字來呼叫 new Person();
- 建構函式的執行流程 A 立刻在堆記憶體中建立一個新的物件 B 將新建的物件設定為函式中的this C 逐個執行函式中的程式碼 D 將新建的物件作為返回值
- 普通函式例子:因為沒有返回值,所以為undefined 建構函式例子:建構函式會馬上建立一個新物件,並將該新物件作為返回值返回
JavaScript 中函式的呼叫有以下幾種方式:作為物件方法呼叫,作為函式呼叫,作為建構函式呼叫,和使用 apply 或 call 呼叫。
- 作為物件呼叫
在 JavaScript 中,函式也是物件,因此函式可以作為一個物件的屬性,此時該函式被稱為該物件的方法,也就是當函式作為方法時,this 被繫結到該物件。
var point = {
x : 0,
y : 0,
moveTo : function(x, y) {
this.x = this.x + x;
this.y = this.y + y;
}
};
point.moveTo(1, 1)//this 繫結到當前物件,即 point 物件
複製程式碼
- 作為函式呼叫
函式直接被呼叫(比如回撥函式),此時 this 繫結到全域性物件。在瀏覽器中,window 就是該全域性物件。
function makeNoSense(x) {
this.x = x;
}
makeNoSense(5);
//執行賦值語句,相當於隱式的宣告瞭一個全域性變數(不好)
x;// x 已經成為一個值為 5 的全域性變數
複製程式碼
對於內部函式,即宣告在另外一個函式體內的函式,這種繫結到全域性物件的方式就會產生問題。
內部函式的 this 應該繫結到其外層函式對應的物件上,可以使用變數替代的方法,該變數一般被命名為 that。
var point = {
x : 0,
y : 0,
moveTo : function(x, y) {
var that = this;
// 內部函式
var moveX = function(x) {
that.x = x;
};
var moveY = function(y) {
that.y = y;
}
moveX(x);
moveY(y);
}
};
point.moveTo(1, 1);
point.x; //==>1
point.y; //==>1
複製程式碼
- 作為建構函式呼叫
JavaScript 支援物件導向式程式設計,與主流的物件導向式程式語言不同,JavaScript 並沒有類(class)的概念,而是使用基於原型(prototype)的繼承方式。
new 一個函式時,背地裡會將建立一個連線到 prototype 成員的新物件,同時this會被繫結到那個新物件上。
- 使用apply或call呼叫
在 JavaScript 中函式也是物件,物件則有方法,apply 和 call 就是函式物件的方法。
apply 和 call 允許切換函式執行的上下文環境(context),即 this 繫結的物件。
bind() 函式會建立一個新函式(稱為繫結函式)
- bind是ES5新增的一個方法,傳參和call或apply類似
- 不會執行對應的函式,call或apply會自動執行對應的函式
- 返回對函式的引用
function Point(x, y){
this.x = x;
this.y = y;
this.moveTo = function(x, y){
this.x = x;
this.y = y;
}
}
//使用建構函式生成了一個物件 p1,該物件同時具有 moveTo 方法;
var p1 = new Point(0, 0);
//使用物件字面量建立了另一個物件 p2
var p2 = {x: 0, y: 0};
//使用 apply 可以將 p1 的方法應用到 p2 上,這時候 this 也被繫結到物件 p2 上
p1.moveTo(1, 1);
p1.moveTo.apply(p2, [10, 10]);
//另一個方法 call 也具備同樣功能,不同的是最後的引數不是作為一個陣列統一傳入,而是分開傳入的。
複製程式碼
總結: this總是指向函式的直接呼叫者(而非間接呼叫者); 作為函式呼叫時,this 繫結到全域性物件;作為方法呼叫時,this 繫結到該方法所屬的物件。 如果有new關鍵字,this指向new出來的那個物件; 在事件中,this指向觸發這個事件的物件,特殊的是,IE中的attachEvent中的this總是指向全域性物件Window;
幾個規則:
//作為函式呼叫
function foo() {
console.log(this.a)
}
var a = 1
foo()
//作為方法呼叫
var obj = {
a: 2,
foo: foo
}
obj.foo()
// 以上兩者情況 `this` 只依賴於呼叫函式前的物件,優先順序是第二個情況大於第一個情況
//作為建構函式呼叫
//以下情況是優先順序最高的,`this` 只會繫結在 `c` 上,不會被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)
// 還有種就是利用 call,apply,bind 改變 this,這個優先順序僅次於 new
複製程式碼
箭頭函式中的this:
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
//呼叫 a 符合前面程式碼中的第一個情況,所以 this 是 window。並且 this 一旦繫結了上下文,就不會被任何程式碼改變。
複製程式碼
箭頭函式其實是沒有 this 的,這個函式中的 this 只取決於他外面的第一個不是箭頭函式的函式的 this。
執行上下文
- 作用域
在ES6之前,js沒有塊級作用域,只有全域性和函式作用域。
注: 其實從 ES3 釋出以來,JavaScript 就有了塊級作用域(with 和 catch分句),而 ES6 引入了 let
變數提升:
console.log(a); //undefined
var a = 2;
//等同於
var a;
console.log(a);
a = 2;
複製程式碼
var a;
是定義宣告,在編譯階段宣告。a = 2;
是賦值宣告,會被留在原地等待執行階段。
函式提升:
在函式作用域中,區域性變數的優先順序比同名的全域性變數高。 函式宣告會被提升,函式表示式不會。
var a;
a = true;
function foo() {
var a;
if(a) {
a = 10;
}
console.log(a);
}
foo(); //undefined
複製程式碼
在 foo(...) {} 的函式作用域中,這個重名區域性變數 a 會遮蔽全域性變數 a,換句話說,在遇到對 a 的賦值宣告之前,在 foo(...) {},a 的值都是 undefined! 所以一個 undefined 的 a 進入不了 if(a) {...} 中,最後被列印出來的是 undefined。
- 執行上下文
-
全域性執行上下文
-
函式執行上下文
-
eval 執行上下文(並不經常使用)
執行棧:也就是在其它程式語言中所說的“呼叫棧”,是一種擁有 LIFO(後進先出)資料結構的棧,被用來儲存程式碼執行時建立的所有執行上下文。
每個執行上下文中都有三個重要的屬性
-
變數物件(VO),包含變數、函式宣告和函式的形參,該屬性只能在全域性上下文中訪問
-
作用域鏈(JS 採用詞法作用域,也就是說變數的作用域是在定義時就決定了)
-
this
var 會產生很多錯誤,所以在 ES6中引入了 let。
let 不能在宣告前使用.
並不是常說的 let 不會提升,let 提升了宣告但沒有賦值,因為臨時死區導致了並不能在宣告前使用。
在提升的過程中,相同的函式會覆蓋上一個函式,並且函式優先於變數提升。
b() // call b second
function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
複製程式碼
- 作用域和執行上下文的關係
幾乎是沒有啥交集。
在一個函式被執行時,建立的執行上下文物件除了儲存了些程式碼執行的資訊,還會把當前的作用域儲存在執行上下文中。所以它們的關係只是儲存關係。
this 的值是通過當前執行上下文中儲存的作用域(物件)來獲取到的。