?這只是個人筆記……我沒想到居然有人看到……
題目來源: 前端開發面試題
答案基本是自己整理的。並不是全部題目都有。
會有很多自己的囉嗦,還有很多亂七八糟的補充,請見諒。
介紹js的基本資料型別(原始資料型別)
Undefined、Null
Boolean、Number、String
//ECMAScript 2015新增:
Symbol //建立後獨一無二且不可變的資料型別
複製程式碼
Symbol
參考:
以下多引自:MDN
Symbol 例項是唯一且不可改變的。
Symbol 物件是 Symbol原始值的封裝。
Symbol 的描述是可選的,但僅用於除錯目的。
Symbol("foo") !== Symbol("foo")
const foo = Symbol()
const bar = Symbol()
typeof foo === "symbol"
typeof bar === "symbol"
複製程式碼
- 為什麼要引入symbol?
ES5 的物件屬性名都是字串,這容易造成屬性名的衝突。
比如,你使用了一個他人提供的物件,但又想為這個物件新增新的方法(mixin 模式),新方法的名字就有可能與現有方法產生衝突。
如果有一種機制,保證每個屬性的名字都是獨一無二的就好了,這樣就從根本上防止屬性名的衝突。
ES6 引入了一種新的原始資料型別Symbol,表示獨一無二的值。
- 生成方式:
Symbol 值通過Symbol函式生成。
let s = Symbol()
typeof s // "symbol"
複製程式碼
Symbol函式前不能使用new命令,否則會報錯。這是因為生成的Symbol是一個原始型別的值,不是物件。
- 引數
description
: 可選。string。對Symbol例項的描述,僅用於除錯。
- 遍歷
ES6之前的方法,無法獲取。
const foo = Symbol()
const bar = Symbol()
let obj = {}
obj[foo] = "foo"
obj[bar] = "bar"
JSON.stringify(obj) // {}
Object.keys(obj) // []
Object.getOwnPropertyNames(obj) // []
Object.getOwnPropertySymbols(obj) // [ Symbol(), Symbol() ]
Reflect.ownKeys(obj) // [Symbol(), Symbol()]
複製程式碼
js有哪些內建物件?
劃分一
基本物件:Object、Function、Boolean、Symbol、Error
數字和日期物件:Number、Math、Date
字串:String、RegExp
集合物件:Array、Map、Set
結構化資料:JSON
控制抽象物件:Promise
其他:arguments
劃分二
Object 是 JavaScript 中所有物件的父物件。
資料封裝類物件:Object、Array、Boolean、Number 和 String
單例內建物件:Math、Global
其他物件:Function、Arguments、Date、RegExp、Error
說幾條寫JavaScript的基本規範?
-
不要在同一行宣告多個變數。
-
使用
===
/!==
來比較true
/false
或者數值- 對於原始資料型別,==和===是有區別的
- 不同型別間比較,==只比較“轉化成同一型別後的值”看“值”是否相等,===如果型別不同,其結果就是不等
- 同型別比較,直接進行“值”比較,兩者結果一樣
- 對於Object,==和===沒有區別, 進行“指標地址”比較
- 基礎型別與Object,==和===有區別
==
將Object轉化為基礎型別,進行“值”比較- 因為型別不同,===結果為false
-
補充點:
==
和型別轉換參考:JavaScript "loose" comparison step by step ←文章還提供了一個很好用的過程展示工具。
判斷流程簡單總結:流程圖
注:關於其中涉及的
toPrimitive
和toNumber
函式,參考:從[]==![]
為true
來剖析JavaScript各種蛋疼的型別轉換 和 MDN
- 對於原始資料型別,==和===是有區別的
-
使用物件字面量替代new Array這種形式
-
不要使用全域性函式
-
Switch語句必須帶有default分支
-
函式不應該有時候有返回值,有時候沒有返回值
-
For迴圈必須使用大括號
-
If語句必須使用大括號
-
for-in迴圈中的變數應該使用var關鍵字明確限定作用域,從而避免作用域汙染。
JavaScript原型,原型鏈 ? 有什麼特點?
原型
All ordinary objects have an internal slot called [[Prototype]].
The value of this internal slot is either null or an object and ++is used for implementing inheritance++.
Data properties of the [[Prototype]] object are inherited (are visible as properties of the child object) for the purposes of get access, but not for set access. Accessor properties are inherited for both get access and set access.
每個物件有一個私有屬性,記作[[prototype]]
。
本質:一個連結到其他物件的引用。
作用:用於繼承。
被連結的物件,是該物件的原型物件。
原型鏈
而原型物件又會有自己的原型物件,這就形成了原型鏈。
原型鏈的頂端:Object.prototype
.(內建了很多功能和方法,包括.toString()
, .valueOf()
, .hasOwnProperty()
, .isPrototypeOf()
等等。)
當我們訪問一個物件的屬性a
時,如果這個物件內部不存在a
這個屬性,那麼就會根據該物件的prototype屬性,去原型物件裡找a
這個屬性。如果原型物件裡還找不到,則會沿著原型鏈一路向上,直到找到這個a
為止;若到達原型鏈頂端,仍未找到,則停止並返回undefined。
疑問:Object.prototype
VS null, 哪個才是原型鏈的末端?
MDN上提到:
By definition, null has no prototype, and acts as the final link in this prototype chain.
但 You Don't Know JS 又說,Object.prototype
是原型鏈的頂端。
這兩種其實本質是一樣的,只是看的方式不同。
就像俄羅斯套娃,最後一個的內部是空的,所以可以斷定它是“最後一個”。如果用這種眼光去看,那麼Object.prototype
就是頂端。
而如果把null也當成是一個節點,那麼“空”才是原型鏈的頂端。
但也有人提出:
ECMA262這麼寫道:
The value of the [[Prototype]] internal slot of the Object prototype object is null.
我不是很懂,因為這個初始值是null,所以就自然成為原型鏈的頂端了嗎
function的prototype屬性
-
關係:
instance.constructor.prototype === instance.__proto__
這個關係不一定正確!!!
比如:
let a = Object.create(null) a.constructor //undefined let b = Object.create(a) b.constructor //undefined 複製程式碼
用
Object.create
的物件,原型鏈上不一定有Object.prototype
,所以也沒有預設的.constructor
、.__proto__
等等。 -
是否所有函式都有
.prototype
屬性Function instances that ++can be used as a constructor++ have a prototype property. Whenever such a function instance is created another ordinary object is also created and is the initial value of the function’s prototype property. Unless otherwise specified, the value of the prototype property is used to initialize the [[Prototype]] internal slot of the object created when that function is invoked as a constructor.
This property has the attributes { [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false }.
NOTE:
//下面寫了沒有這個屬性的三種情況
Function objects created using
Function.prototype.bind
, or by evaluating a MethodDefinition (that are not a GeneratorMethod) or an ArrowFunction grammar production do not have a prototype property.可以當作建構函式的function都有一個屬性prototype,這個屬性預設指向一個JS私有物件
%FunctionPrototype%
。 -
.prototype
屬性和[[prototype]]
的關係 -
也是
-
改變
[[prototype]]
的兩種方法When a constructor creates an object, that object implicitly references the constructor’s prototype property for the purpose of resolving property references. The constructor’s prototype property can be referenced by the program expression constructor.prototype, and properties added to an object’s prototype are shared, through inheritance, by all objects sharing the prototype.
Alternatively, a new object may be created with an explicitly specified prototype by using the
Object.create
built-in function.1、使用建構函式建立
obj
,會隱式地引用建構函式的prototype
屬性,作為obj
的[[prototype]]
。(可以這麼理解:
obj.[[prototype]] = objConstructor.prototype
)2、
Object.create(O)
可建立一個obj,並指定它的[[prototype]]
為O
。- 另外兩種改變
[[prototype]]
的方法:詳見MDN- ES6: Object.setPrototypeOf
- proto(不推薦使用這個屬性)
- 另外兩種改變
特點:
JavaScript物件是通過引用來傳遞的,我們建立的每個新物件實體中並沒有一份屬於自己的原型副本。當我們修改原型時,與之相關的物件也會繼承這一改變。
當我們需要一個屬性的時,Javascript引擎會先看當前物件中是否有這個屬性, 如果沒有的話, 就會查詢他的Prototype物件是否有這個屬性,如此遞推下去,一直檢索到 Object 內建物件。
function Func() {}
Func.prototype.name = "Sean";
Func.prototype.getInfo = function() { return this.name; }
// pre ES5.1
var person = new Func();
//ES5.1
// var person = Object.create(Func.prototype);
//ES6+
// Object.setPrototypeOf(person, Func.prototype);
//它擁有了Func的屬性和方法
console.log(person.getInfo()); //"Sean"
console.log(Func.prototype); // Func { name="Sean", getInfo=function()}
複製程式碼
JavaScript有幾種型別的值?你能畫一下他們的記憶體圖嗎?
- 原始資料型別(Undefined,Null,Boolean,Number、String)
- 引用資料型別(物件、陣列和函式)
兩種型別的區別:儲存位置不同。
區別 | 原始資料型別 | 引用資料型別 |
---|---|---|
儲存位置 | 棧(stack) | 堆(heap) |
佔據空間 | 小,大小固定 | 大,大小不固定 |
引用資料型別如果儲存在棧中,將會影響程式執行的效能;
引用資料型別在棧中儲存了指標,該指標指向堆中該實體的起始地址。當直譯器尋找引用值時,會首先檢索其在棧中的地址,取得地址後從堆中獲得實體。
如何將字串轉化為數字,例如'12.3b'?
1. 轉換函式
// radix = 0, 2, 3, ..., 36
// radix表示的是string的進位制,而非轉換出來的結果的進位制
parseInt(string, radix)
parseFloat(value)
複製程式碼
都為全域性函式,不屬於任何物件。(但Number裡有同名函式)
能轉換的資料型別:
- String
- 定義了 toString 或者 valueOf 方法的物件
Examples:
parseInt('36', 36) // 114
parseInt('36', 37) // NaN
parseInt('36', 1) // NaN
parseInt('36', 0) // 36。radix為0或null,則預設為十進位制
let obj = {}
obj.toString = function () { return '3'; }
parseInt(obj) // 3
複製程式碼
2. 強制型別轉換(type casting)
Number(value)
複製程式碼
在非構造器上下文中 (如:沒有 new 操作符),Number 能被用來執行型別轉換。
3. 正規表示式 + 隱式轉換
'12.3b'.match(/(\d)+(\.)?(\d)+/g)[0] * 1
複製程式碼
但是這個不太靠譜,提供一種思路而已。
如何將浮點數點左邊的數每三位新增一個逗號,如12000000.11轉化為『12,000,000.11』?
方法一、toLocaleString
參考:
var number = 12345.543;
number.toLocaleString('en'); // "12,345.543"
number = 123.1;
number.toLocaleString('en'); // "123.1"
number = 12345.54321;
number.toLocaleString('en'); // "12,345.543"
複製程式碼
侷限:只顯示三位小數,可通過maximumSignificantDigits
這個引數調整,但老的瀏覽器不支援此引數。
方法二、正規表示式
參考:javascript 正則(將數字轉化為三位分隔的樣式)
不易懂,但相容性強。
寫法一:
// 匹配邊界,直接可替換成逗號
function commafy (num) {
return num
.toString()
.replace(/\B(?=(?:\d{3})+\.)/g, ',')
}
複製程式碼
寫法二:
// 匹配邊界前的那個數字,所以不能直接替換,
// 要保留數字,在數字後加一個逗號。
function commafy(num){
return num && num
.toString()
.replace(/(\d)(?=(\d{3})+\.)/g, function($1, $2){
return $2 + ',';
});
}
// 也可以這麼寫(不用function用string)
replace(/(\d)(?=(\d{3})+\.)/g, '$1,')
複製程式碼
如何實現陣列的隨機排序?
方法一:Knuth-Fisher-Yates shuffle algorithm
The de-facto(事實上) unbiased(無偏的) shuffle algorithm.(參考)
function shuffle(array) {
var currentIndex = array.length,
temporaryValue,
randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
// Used like so
var arr = [2, 11, 37, 42];
arr = shuffle(arr);
console.log(arr);
複製程式碼
!!!注意:The Danger of Naïveté
注意以下兩者的區別!
// naïve algorithm
// i = 0, 1, ..., len-1 (迴圈len次)
// 每次rand.Next()的值不變,都為cards.Length
for (int i = 0; i < cards.Length; i++) {
int n = rand.Next(cards.Length);
Swap(ref cards[i], ref cards[n]);
}
// correct Knuth-Fisher-Yates shuffle algorithm
// i = 1, 2, ..., len-1 (迴圈len-1次)
// 每次rand.Next()的值都不同,為 i + 1 (即2, 3, ..., len)
for (int i = cards.Length - 1; i > 0; i--) {
int n = rand.Next(i + 1);
Swap(ref cards[i], ref cards[n]);
}
複製程式碼
The naive shuffle results in 33 (27) possible deck combinations. That's odd, because the mathematics tell us that there are really only 3! or 6 possible combinations of a 3 card deck.
In the KFY shuffle, we start with an initial order, swap from the third position with any of the three cards, then swap again from the second position with the remaining two cards.
naive shuffle: 3次rand.Next(3)
,也就是說,每次都是 0, 1, 2
三個數中,隨機選一個。也就是總的結果有==3^3=27==種。
KFY shuffle:只有是rand.Next(3)
和是rand.Next(2)
,第一次是 0, 1, 2
三個數中,隨機選一個,第二次是 0, 1
兩個數中選一個。結果為==3!=6==種。
本質:完美洗牌的演算法問題。
這個問題需要剔除給定的array的初始順序的影響。
無論怎麼洗,出現各種結果的概率必須是一樣的。
這樣其實問題就簡單:只要考慮,每個index的位置,有多少種可能性就行了。
這就像是:餐桌四個座位,有幾種坐法?
其實就是array的排列問題。
方法二:push + 刪減
var arr = [1,2,3,4,5,6,7,8,9,10];
function randSort2(arr){
var mixedArray = [];
while(arr.length > 0){
var randomIndex = parseInt(Math.random()*arr.length);
mixedArray.push(arr[randomIndex]);
// 刪除已push的專案
arr.splice(randomIndex, 1);
}
return mixedArray;
}
console.log(randSort2(arr));
複製程式碼
方法三:sort
Array.prototype.sort([compareFunction])
: 根據所提供的排序演算法,進行排序。
compareFunction(a, b)
接受兩個引數,即兩個比較的item。
函式返回一個number:
- 結果為0則保持不變
- 小於0,a在前,b在後
- 大於0,a在後,b在前
var arr = [1,2,3,4,5,6,7,8,9,10];
arr.sort(function(){
return Math.random() - 0.5;
})
console.log(arr);
複製程式碼
-
缺陷:
這個方法產生的結果,並不是均勻的。
測試如下:
function randSort(){ var sum = Array.apply(null, { length: 10 }).fill(0); var AVG_TIMES = 1000; var SHUFFLE_TIMES = 10000; var avgTimes = AVG_TIMES; while (avgTimes) { var times = SHUFFLE_TIMES; var counts = Array.apply(null, { length: 10 }).fill(0); // 計算index0裡各個數字的出現概率 while (times) { var someArray = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; someArray.sort(() => Math.random() - 0.5); counts[someArray[0]]++; times--; } //console.log(counts); sum = sum.map(function (v, i) { return v + counts[i]; }); avgTimes--; } console.log(sum.map(function (v, i) { return v / AVG_TIMES; })); } 複製程式碼
測試了shuffle後的array[0]的取值中,各個數字在10000次測試中的平均出現次數。
1000 * 10000 次的平均結果是這樣:
[1953.077, 725.763, 963.728, 1287.405, 829.722, 893.497, 990.991, 1115.342, 599.359, 641.116] 複製程式碼
javascript建立物件的幾種方式?
參考:高程三 6.2
0、Object 建構函式或物件字面量
可以用來建立單個物件。
但要建立很多物件,就會產生大量的重複程式碼。
// 建立 Object 的例項
var person = new Object();
person.firstname = 'Captain';
person.lastname = 'Flag';
person.sayName = function sayName () {
console.log(this.firstname + ' ' + this.lastname);
};
// 字面量
var person = {
firstname: 'Captain',
lastname: 'Flag',
sayName: function sayName () {
console.log(this.firstname + ' ' + this.lastname);
}
};
複製程式碼
1、工廠模式
工廠模式是軟體工程領域一種廣為人知的設計模式,這種模式抽象了建立具體物件的過程(本書後 面還將討論其他設計模式及其在 JavaScript 中的實現)。考慮到在 ECMAScript 中無法建立類,開發人員 就發明了一種函式,用函式來封裝以特定介面建立物件的細節
function createPerson (firstname, lastname) {
var o = new Object ();
o.firstname = firstname;
o.lastname = lastname;
o.sayName = function sayName () {
console.log(this.firstname + ' ' + this.lastname);
};
return o;
}
var cap = createPerson('Captain', 'Flag');
複製程式碼
- 優點:解決了建立多個相似物件的問題。
- 缺點:沒有解決物件識別的問題(即怎樣知道一個物件的型別)。
2、建構函式模式
function Person (firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
this.sayName = function sayName () {
console.log(this.firstname + ' ' + this.lastname);
};
}
var cap = new Person('Captain', 'Flag');
複製程式碼
new 操作符呼叫建構函式的過程:
- 建立一個新物件;
- 將新物件的
[[protorype]]
設定為Person.prototype
; - 建構函式
Person
被傳入引數並呼叫(執行建構函式中的程式碼,為這個新物件新增屬性),關鍵字this
被設定指向新物件; - 除非函式
Person
顯性返回一個物件,否則自動返回新物件。
-
優點:
有解決物件識別的問題,可以將例項標識為一種特定的型別。
-
缺點:
無法進行函式複用。(以這種方式建立函式,會導致不同的作用域鏈和識別符號解析,但建立 Function 新例項的機制仍然是相同的。因此,不同例項上的同名函式是不相等的。但建立兩個完成同樣任務的 Function 例項的確沒有必要。)
解決:把函式定義轉移到建構函式外部。
function sayName () { console.log(this.firstname + ' ' + this.lastname); } function Person (firstname, lastname) { this.firstname = firstname; this.lastname = lastname; this.sayName = sayName; } 複製程式碼
新的問題:破壞封裝。
3、原型模式
可以當作建構函式的function都有一個屬性prototype,這個屬性是一個指標,指向一個物件,而這個物件的用途是包含可以由特定型別的所有例項共享的屬性和方法。
function Person () {}
Person.prototype.firstname = 'Captain';
Person.prototype.lastname = 'Flag';
Person.prototype.sayName = function sayName () {
console.log('Captain Flag');
};
var cap = new Person();
// 更簡單的原型語法
function Person () {}
Person.prototype = {
// 這種寫法會將預設的Person.prototype完全覆蓋重寫,
// Person也不會有`constructor`屬性
// `Person.prototype.hasOwnProperty('constructor')`的結果為false
// 如果仍需要`constructor`屬性,自行增加
constructor: Person,
firstname: 'Captain',
lastname: 'Flag',
sayName: function sayName () {
console.log('Captain Flag');
}
};
// 以上一種方式重設 constructor 屬性會導致它的[[Enumerable]]特性被設定為 true
// 預設情況下,原生的 constructor 屬性是不可列舉的
// 相容 ECMAScript 5 的 JavaScript 引擎上,可用 Object.defineProperty() 修復
function Person () {}
Person.prototype = {
firstname: 'Captain',
lastname: 'Flag',
sayName: function sayName () {
console.log('Captain Flag');
}
};
// 重設constructor屬性,只適用於 ECMAScript 5 相容的瀏覽器
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
});
複製程式碼
注:後兩者寫法,都直接重寫了原型,如果有例項在原型重寫之前生成,這些例項與更改後的原型是沒有聯絡的。
-
優點:
可以讓所有物件例項共享原型物件所包含的屬性和方法(不必在建構函式中定義物件例項的資訊,而是可以將這些資訊直接新增到原型物件中)。
-
缺點:
- 不能傳遞引數。
- 原型模式的最大問題是由其共享的本性所導致的。原型中所有屬性是被很多例項共享的。
- 這種共享十分適用於函式,
- 對於原始資料型別尚可,
- 但引用型別在原型中也只是保留了一個指標,在例項中被修改後,會影響原型的值。
function Person () {} Person.prototype.arr = [1,2,3]; var cap = new Person(); cap.arr.push('a'); console.log(Person.prototype.arr); // [1, 2, 3, "a"] 複製程式碼
4、組合使用建構函式模式和原型模式
模式 | 所定義的內容 |
---|---|
建構函式模式 | 例項屬性 |
原型模式 | 方法和共享的屬性 |
定義引用型別的一種預設模式。
function Person (firstname, lastname) {
this.name = [firstname, lastname];
}
Person.prototype.sayName = function sayName () {
console.log(this.name.join(' '));
};
var cap = new Person('Captain', 'Flag');
cap.name.push('1911');
cap.sayName(); // Captain Flag 1911
複製程式碼
-
優點:
- 支援向建構函式傳遞引數。
- 每個例項都會有自己的一份例項屬性的副本,但同時又共享著對方法的引用,最大限度地節省了記憶體。
5、動態原型模式
function Person (firstname, lastname) {
this.name = [firstname, lastname];
// 只在 sayName() 方法不存在的情況下,才會將它新增到原型中。
// 這段程式碼只會在初次呼叫建構函式時才會執行。
// 此後,原型已經完成初始化,不需要再做什麼修改了。
// 其中, if 語句檢查的,可以是初始化之後應該存在的任何屬性或方法,
// 並且不必檢查每個屬性和每個方法,只要檢查其中一個即可。
if (typeof this.sayName !== 'function') {
Person.prototype.sayName = function sayName () {
console.log(this.name.join(' '));
};
}
}
var cap = new Person('Captain', 'Flag');
cap.name.push('1911');
cap.sayName(); // Captain Flag 1911
複製程式碼
-
優點:
有其他 OO 語言經驗的開發人員在看到獨立的建構函式和原型時,很可能會感到非常困惑。動態原 型模式正是致力於解決這個問題的一個方案,它把所有資訊都封裝在了建構函式中,而通過在建構函式 中初始化原型(僅在必要的情況下),又保持了同時使用建構函式和原型的優點。換句話說,可以通過 檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。
- 對於採用這種模式建立的物件,還可以使用 instanceof 操作符確定它的型別
6、寄生建構函式模式
基本思想是建立一個函式,該函式的作用僅僅是封裝建立物件的程式碼,然後再返回新建立的物件;但從表面上看,這個函式又很像是典型的建構函式。
function Person (firstname, lastname) {
var o = new Object ();
o.firstname = firstname;
o.lastname = lastname;
o.sayName = function sayName () {
console.log(this.firstname + ' ' + this.lastname);
};
return o;
}
var cap = new Person('Captain', 'Flag');
複製程式碼
工廠模式 + 建構函式(除了使用 new 操作符並把使用的包裝函式叫做建構函式之外,這個模式跟工廠模式其實是一模一樣的。)
建構函式在不返回值的情況下,預設會返回新物件例項。而通過在建構函式的末尾新增一個 return 語句,可以重寫呼叫建構函式時返回的值。
-
優點:
在特殊的情況下用來為物件建立建構函式。
比如:建立一個具有額外方法的特殊陣列。由於不能直接修改 Array 建構函式,因此可以使用這個模式。
-
缺點:
返回的物件與建構函式或者與建構函式的原型屬性之間沒有關係。不能依賴 instanceof 操作符來確定物件型別。
7、穩妥建構函式模式
所謂穩妥物件,指的是沒有公共屬性,而且其方法也不引用 this 的物件。穩妥物件最適合在一些安全的環境中(這些環境中會禁止使用 this 和 new),或者在防止資料被其他應用程式(如 Mashup 程式)改動時使用。
穩妥建構函式遵循與寄生建構函式類似的模式,但有兩點不同:
- 一是新建立物件的例項方法不引用 this;
- 二是不使用 new 操作符呼叫建構函式。
function Person (firstname, lastname) {
// 建立要返回的物件
var o = new Object ();
// 定義私有變數和函式
o.firstname = firstname;
o.lastname = lastname;
// 新增方法
o.sayName = function sayName () {
console.log(this.firstname + ' ' + this.lastname);
};
// 返回物件
return o;
}
var cap = Person('Captain', 'Flag');
複製程式碼
-
優點:
安全性高,使得它非常適合在某些安全執行環境。
-
缺點:
與寄生建構函式模式類似,建立的物件與建構函式之間也沒有什麼關係,因此 instanceof 操作符對這種物件也沒有意義。
Javascript如何實現繼承?
參考:高程三 6.3
現在有一個"動物"物件的建構函式。
function Animal () {
this.kingdom = 'animal'; // 動物界
}
複製程式碼
還有一個"貓"物件的建構函式。
function Cat () {
this.family = 'cat'; // 貓科
}
複製程式碼
1、原型繼承
ECMAScript 中描述了原型鏈的概念,並將原型鏈作為實現繼承的主要方法。其基本思想是利用原型 讓一個引用型別繼承另一個引用型別的屬性和方法。
如果"貓"的prototype物件,指向一個Animal的例項,那麼所有"貓"的例項,就能繼承Animal了。
// 刪除了 prototype 物件原先的值
// 將Cat的prototype物件指向一個Animal的例項
Cat.prototype = new Animal();
// 任何一個prototype物件都有一個constructor屬性,指向它的建構函式
// 如果沒有上一行,Cat.prototype.constructor 指向Cat
// 加了這一行以後,Cat.prototype.constructor指向Animal
// 這顯然會導致繼承鏈的紊亂(cat明明建構函式Cat生成的),因此我們必須手動糾正
Cat.prototype.constructor = Cat;
var cat = new Cat();
console.log(cat.kingdom); // 'animal'
複製程式碼
缺點:
- 需要執行和建立Animal的例項,消耗記憶體,效率較低;
- 原型屬性會被所有例項共享,在子型別中更改引用型別資料的值,會影響原型的值;
- 在建立子型別(Cat)的例項時,沒有辦法在不影響所有物件例項的情況下,給超型別(Animal)的建構函式傳遞引數。
2、借用建構函式(constructor stealing)/偽造物件/經典繼承
在子型別建構函式的內部呼叫超型別建構函式。
函式只不過是在特定環境中執行程式碼的物件,因此通過使用 apply()
和 call()
方法也可以在(將來)新建立的物件上執行建構函式。
使用call或apply方法,將父物件的建構函式繫結在子物件上。
即在子物件建構函式中加一行:
function Cat () {
Animal.apply(this); // “借調”超型別的建構函式
// or
// Animal.call(this);
this.family = 'cat';
}
複製程式碼
-
優點:
-
簡單。
-
可以在子型別建構函式中向超型別建構函式傳遞引數,如:
function Animal (name) { this.name = name; } function Cat (name) { this.kind = 'cat'; Animal.call(this, name); } 複製程式碼
-
-
缺點:
- 如果僅僅是借用建構函式,那麼也將無法避免建構函式模式(建立物件的一種方式)存在的問題 —— 方法都在建構函式中定義,因此函式複用就無從談起了。
- 在超型別的原型
Animal.prototype
中定義的方法(因為並沒有利用原型鏈),對子型別而言也是不可見的,結果所有型別都只能使用建構函式模式。
3、組合繼承(combination inheritance)/偽經典繼承
JavaScript 中最常用的繼承模式。
結合前兩種方法,發揮二者之長的一種繼承模式。
思路:
- 原型鏈:繼承 原型屬性和方法,
- 借用建構函式:繼承 例項屬性。(在子型別中更改其值,不會影響原型的值)
這樣,既通過在原型上定義方法實現了函式複用,又能夠保證每個例項都有它自己的屬性。
function Animal (name) {
this.name = name;
}
Animal.prototype.sayNamw = function () {
console.log(this.name);
};
function Cat (name) {
Animal.call(this, name); // 第二次呼叫超類建構函式
}
Cat.prototype = new Animal(); // 第一次呼叫超類建構函式
Cat.prototype.constructor = Cat;
// 使用
var cat1 = new Cat('Meow');
cat1.name; // 'Meow'
cat1.name = 'Oops'; // 'Oops'
Animal.prototype.name; // undefined(改變子類的值,不會改變原型的值)
cat1.sayName(); // 'Oops'
Animal.prototype.sayName(); // undefined
複製程式碼
instanceof
和 isPrototypeOf()
能夠識別 基於組合繼承建立的物件。
-
缺點:
-
無論什麼情況下,都會呼叫兩次超型別建構函式:一次是在建立子型別原型的時候,另一次是在子型別建構函式內部。
-
屬性也會建立兩遍:
- 第一次呼叫
Animal
建構函式時,Cat.prototype
會得到name屬性; - 第二次呼叫
Animal
建構函式,在新物件上cat1
上建立了例項屬性name
。 - 於是,這個屬性就遮蔽了原型中的同名屬性。
- 第一次呼叫
-
4、原型式繼承
這種方法並沒有使用嚴格意義上的建構函式。
藉助原型可以基於已有的物件建立新物件,同時還不必因此建立自定義型別。
- ECMAScript 5 通過新增
Object.create()
方法規範化了原型式繼承。
function object (o) {
function F () {}
F.prototype = o;
return new F();
}
複製程式碼
object(o)
的作用是,以o為原型,建立一個新的物件。比如:
var oldObj = { a: 1 };
var newObj = object(oldObj);
複製程式碼
具體關係可見下圖:
graph LR
A(oldObj) -->|__proto__| B(F.prototype)
B -->|__proto__| C(newObj)
D[F] -->|prototype| B
複製程式碼
ECMAScript 5 通過新增Object.create()
方法規範化了原型式繼承。
這個方法接收兩個引數:一 個用作新物件原型的物件,(可選的)一個為新物件定義額外屬性的物件。
-
缺點:
- 包含引用型別值的屬性始終都會共享相應的值,就像使用原型模式一樣。
var arr = ['old']; var oldObj = { arr: arr }; var newObj = object(oldObj); newObj.arr.push('new'); // 會更改原值 console.log(arr); // ['old', 'new'] newObj.arr = [0, 1]; // 不會更改原值 console.log(newObj.arr); // [0, 1] console.log(arr); // ['old', 'new'] 複製程式碼
- 包含引用型別值的屬性始終都會共享相應的值,就像使用原型模式一樣。
5、寄生式(parasitic)繼承
與原型式繼承(上一種) 緊密相關的一種思路。
與寄生建構函式和工廠模式類似。
建立一個僅用於封裝繼承過程的函式,該函式在內部以某種方式來增強物件,最後再像真地是它做了所有工作一樣返回物件。
// 原型式繼承的主要方法
function object (o) {
function F () {}
F.prototype = o;
return new F();
}
function createAnother (original) {
// object()函式不是必需的;
// 任何能夠返回新物件的函式都適用於此模式。
var clone = object(original); //通過呼叫函式建立一個新物件
clone.sayHi = function(){ //以某種方式來增強這個物件
console.log("hi");
};
return clone; //返回這個物件
}
複製程式碼
在主要考慮物件而不是自定義型別和建構函式的情況下,寄生式繼承也是一種有用的模式。
-
缺點:
- 使用寄生式繼承來為物件新增函式,會由於不能做到函式複用而降低效率;這一點與建構函式模式類似。
6、寄生組合式繼承
解決3、組合繼承
的缺點 —— 呼叫兩次超型別建構函式 ∴ 屬性也建立了兩遍。
借用建構函式來繼承屬性,通過原型鏈的混成形式來繼承方法。
-
基本思路:
不必為了 指定子型別的原型 而 呼叫超型別的建構函式,我們所需要的無非就是超型別原型的一個副本而已。
-
本質:
使用 寄生式繼承 來 繼承超型別的原型,然後再將結果指定給子型別的原型。
/*
* @param {constructor} subType 子型別建構函式
* @param {constructor} superType 超型別建構函式
*/
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //建立物件
prototype.constructor = subType; //增強物件
subType.prototype = prototype; //指定物件
}
複製程式碼
第一步是建立超型別原型的一個副本。
第二步是為建立的副本新增 constructor 屬性,從而彌補因重寫原型而失去的預設的 constructor 屬性。
最後一步,將新建立的物件(即副本)賦值給子型別的原型。這樣,我們就可以用呼叫 inheritPrototype()
函式的語句,去替換前面例子中為子型別原型賦值的語句了。
用inheritPrototype
替代3、組合繼承
中,第一次呼叫建構函式時的Cat.prototype = new Animal();
:
function Animal (name) {
this.name = name;
}
Animal.prototype.sayNamw = function () {
console.log(this.name);
};
function Cat (name) {
Animal.call(this, name); // 這裡保持不變,依舊會呼叫超類建構函式
}
//Cat.prototype = new Animal(); // 第一次呼叫超類建構函式
//Cat.prototype.constructor = Cat;
inheritPrototype(Cat, Animal);
複製程式碼
-
優點:
-
高效率,體現在它只呼叫了一次超類建構函式,因此避免了在
Cat.prototype
上建立不必要的、多餘的屬性,並且原型鏈還能保持不變還能夠正常使用 instanceof 和 isPrototypeOf()。 -
開發人員普遍認為寄生組合式繼承是引用型別最理想的繼承正規化。
-
Javascript作用鏈域?
全域性函式無法檢視區域性函式的內部細節,但區域性函式可以檢視其上層的函式細節,直至全域性細節。
當需要從區域性函式查詢某一屬性或方法時,如果當前作用域沒有找到,就會上溯到上層作用域查詢,直至全域性函式。
這種組織形式就是作用域鏈。
談談This物件的理解。
- this總是指向函式的直接呼叫者(而非間接呼叫者);
- 如果有new關鍵字,this指向new出來的那個物件;
- 在事件中,this指向觸發這個事件的物件,特殊的是,IE中的attachEvent中的this總是指向全域性物件Window。
以下內容總結自You Don't Know JS:
規則1. 預設繫結(Default Binding)
其他任何規則都無法應用時,就會使用預設繫結。
預設繫結情況下,this
在非嚴格模式中指向global
,嚴格模式中為undefined
。
function foo () {
'use strict';
console.log(this.a);
}
foo(); //Uncaught TypeError: Cannot read property 'a' of undefined
複製程式碼
規則2. 隱式繫結
考慮呼叫位置(call-site)是否有上下文物件(context object)。
this
總是指向函式的直接呼叫者(而非間接呼叫者)。
var obj = {
a: 1,
foo: function () {
console.log(this.a);
}
};
obj.foo(); // 1
複製程式碼
鏈式呼叫,呼叫的是最後一個物件的屬性:
var obj1 = {
a: 1,
obj2: obj2
};
var obj2 = {
a: 2,
foo: function foo () {
console.log(this.a);
}
};
obj1.obj2.foo(); // 2
複製程式碼
隱式丟失(Implicitly Lost)
-
function reference/alias(函式引用)
var obj = { a: 1, foo: function () { console.log(this.a); } }; var bar = obj.foo; var a = 'oops, global'; bar(); // 'oops, global' 複製程式碼
其實,bar就是一個函式foo的引用而已。
它的呼叫位置其實是全域性環境(預設繫結規則被應用)。
graph TD A[執行的函式: foo/bar] --> B[call-site: 全域性環境] 複製程式碼
-
passing a callback function(傳遞迴調函式)
function foo () { console.log(this.a); } function doSth (fn) { fn(); } var obj = { a: 1, foo: foo }; var a = 'oops, global'; doSth(obj.foo); // 這裡其實就是對fn的賦值(LHS),本質同上一情形 複製程式碼
規則3. 顯式繫結(Explicit Binding)
使用call()
或apply()
。
function foo () {
console.log(this.a);
}
var obj = {
a: 2
};
foo.call( obj );
複製程式碼
修正隱式繫結中的兩個例子:
1.
javascript var obj = { a: 1, foo: function () { console.log(this.a); } }; var bar = obj.foo; var a = 'oops, global'; bar(); // 'oops, global' bar.call(obj); // 1
2.
```javascript
function foo () {
console.log(this.a);
}
function doSth (fn) {
//fn();
fn.call(obj); // 顯式繫結
}
var obj = {
a: 1,
foo: foo
};
var a = 'oops, global';
doSth(obj.foo); // 這裡其實就是對fn的賦值(LHS),本質同上一情形
```
複製程式碼
解決隱式繫結this丟失問題:Hard binding
上述的修正只是在呼叫的時候做的修正,沒有真正解決問題。
-
包裝成一個函式:
function foo () { console.log(this.a); } var obj = { a: 2 }; /* hard binding */ var bar = function () { foo.call( obj ); }; /* 如論怎麼呼叫bar,都會呼叫obj.foo */ bar();// 2 setTimeout(bar, 100);// 2 bar.call(window);// 2 複製程式碼
傳參:
function foo (b) { console.log(this.a, b); } var obj = { a: 1 }; var bar = function () { foo.apply(obj, arguments); }; bar(3); // 1 3 複製程式碼
-
reusable helper
function foo (b) { console.log(this.a, b); } var obj = { a: 1 }; // 可進行重複利用 function bind (fn, obj) { return function () { fn.apply(obj, arguments); }; } var bar = bind(foo, obj); bar(3); // 1 3 複製程式碼
ES5內建
Function.prototype.bind
:function foo (b) { console.log(this.a, b); } var obj = { a: 1 }; var bar = foo.bind(obj); bar(3); // 1 3 複製程式碼
-
一些API提供thisArg引數
function foo (el) { console.log(el, this.a); } var obj = { a: 'a' [1, 2, 3].forEach(foo, obj); // 1 "a" // 2 "a" // 3 "a" 複製程式碼
規則4. new繫結(new Binding)
將this指向新建的物件。
規則覆蓋順序
3顯式、4new > 2隱式 > 1基礎
例外
-
this被忽略
傳遞
null
或undefined
作為apply
、call
或bind
的繫結引數,this繫結會被忽略,而應用預設繫結。-
使用場景:
function foo () { // arguments為類陣列方法,不能直接呼叫join方法 console.log(Array.prototype.join.call(arguments, ', ')) } // 陣列轉化為引數 foo.apply(null, [1,2]); // 1, 2 foo.apply(null, [1,2,3,4,5]); // 1, 2, 3, 4, 5 複製程式碼
-
可能存在的問題:
使用第三方庫時,可能內部存在
this
,從而不經意中指向了global
。 -
更安全的this:
使用DMZ物件(一個完全空的、沒有任何繼承的物件)。
function foo () { console.log(this); console.log(Array.prototype.join.call(arguments, ', ')); } // DMZ empty object var Ø = Object.create(null); foo.apply(Ø, [1,2]) // {} No properties // 1, 2 foo.apply(null, [1,2]) // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …} // 1, 2 複製程式碼
-
-
間接引用
退回預設繫結。
-
知識點:
變數宣告返回undefined,賦值返回等號右邊的值。
-
例子:
function foo () { console.log(this.a); } var a = 2; var o = { a: 3, foo: foo }; var p = { a: 4 }; (p.foo = o.foo)(); // 2 複製程式碼
why?
p.foo = o.foo
預設返回foo函式,這個foo函式才是立即執行的物件。p.foo = o.foo //ƒ foo () { // console.log(this.a); //} 複製程式碼
-
-
軟繫結(Softening Binding)
複雜,暫時不想管。
詞法this(Lexical this)
this是在定義函式時繫結的,不是在執行過程中繫結的。
-
方法一:詞法捕獲this
function foo () { var self = this; return function () { console.log(self.a); }; } var obj1 = { a: 1 }; var obj2 = { a: 2 }; var bar = foo.call(obj1); bar(); // 1 bar.call(obj2); // 1 複製程式碼
-
方法二:ES6 箭頭函式
幾乎與詞法捕獲相同。
function foo () { return () => { console.log(this.a); }; } var obj1 = { a: 1 }; var obj2 = { a: 2 }; var bar = foo.call(obj1); bar(); // 1 bar.call(obj2); // 1 複製程式碼
注意:儘量不要混用詞法方法和預設的this機制(rule1-4)。
new操作符具體幹了什麼呢?
- 建立一個新物件;
- 將新物件的
[[protorype]]
設定為Base.prototype
; - 建構函式
Base
被傳入引數並呼叫(執行建構函式中的程式碼,為這個新物件新增屬性),關鍵字this
被設定指向新物件; - 除非函式
Base
顯性返回一個物件,否則自動返回新物件。
new Base();
// 即
var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj);
複製程式碼
Ajax 是什麼? 如何建立一個Ajax?
參考: MDN,高程三
ajax的全稱:Asynchronous Javascript And XML(非同步傳輸+js+xml)。
其本身不是一種新技術,而是一個在 2005年被Jesse James Garrett提出的新術語,用來描述一種使用現有技術集合的‘新’方法。包括:
- HTML or XHTML
- CSS(Cascading Style Sheets)
- JavaScript
- DOM(The Document Object Model)
- XML
- XSLT
- 最重要的 XMLHttpRequest object
AJAX主要特徵:
- Make requests to the server without reloading the page
- Receive and work with data from the server
AJAX可與伺服器交流並交換資料,無需重新重新整理頁面就能更新頁面。
所謂非同步,在這裡簡單地解釋就是:向伺服器傳送請求的時候,我們不必等待結果,而是可以同時做其他的事情,等到有了結果它自己會根據設定進行後續操作,與此同時,頁面是不會發生整頁重新整理的,提高了使用者體驗。
ajax api總體程式碼
function ajax (callback, requestMethod, url, isAsync, data) {
// create an instance of XMLHttpRequest
let xhr = createXHR()
// handle the server response
xhr.onreadystatechange = function handleRequest () {
// check the request's state
if (xhr.readyState === XMLHttpRequest.DONE) { // 4
// when the response's received
callback(xhr)
} else {
// not ready yet
}
}
// actually make the request
xhr.open(requestMethod, url, isAsync)
if (requestMethod === 'POST') {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.send(data)
} else {
xhr.send()
}
}
function createXHR () {
if (typeof XMLHttpRequest !== 'undefined') { // IE7+ and other
return new XMLHttpRequest()
} else if (typeof ActiveXObject !== 'undefined') { // IE6 and older
// create the newest XHR object in MSXML
if (typeof createXHR.activeXString !== 'string') {
const versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp']
for (let i = 0, len = versions.length; i < len; i++) {
try {
new ActiveXObject(versions[i])
createXHR.activeXString = versions[i]
break
} catch (e) {}
}
}
return new ActiveXObject(createXHR.activeXString)
} else {
throw new Error('No XHR object available.')
}
}
複製程式碼
步驟:
-
建立XMLHttpRequest物件,也就是建立一個非同步呼叫物件
// Old compatibility code, no longer needed. function createXHR () { if (typeof XMLHttpRequest !== 'undefined') { // IE7+ and other return new XMLHttpRequest() } else if (typeof ActiveXObject !== 'undefined') { // IE6 and older // create the newest XHR object in MSXML // if中的語句只需執行一次 // 第一次執行後,會在函式createXHR上建立一個屬性activeXString // 當第二次執行時,只要該屬性不被改寫 // 便仍可使用 // 檢驗其是否存在,存在則直接跳過這段if if (typeof createXHR.activeXString !== 'string') { const versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'] for (let i = 0, len = versions.length; i < len; i++) { // 檢驗建立ActiveXObject例項是否會報錯 // 並不是真正地建立 try { new ActiveXObject(versions[i]) createXHR.activeXString = versions[i] break } catch (e) {} } } // 此處根據activeXString屬性值,建立ActiveXObject例項 return new ActiveXObject(createXHR.activeXString) } else { throw new Error('No XHR object available.') } } 複製程式碼
-
建立一個新的HTTP請求,並指定該HTTP請求的方法、URL及驗證資訊
// actually make the request xhr.open(requestMethod, url, isAsync) 複製程式碼
-
設定響應HTTP請求狀態變化的函式
XHR 物件的 readyState 屬性,該屬性表示請求/響應過程的當前活動階段。這個屬性可取的值如下:
- 0:未初始化。尚未呼叫 open()方法。
- 1:啟動。已經呼叫 open()方法,但尚未呼叫 send()方法。
- 2:傳送。已經呼叫 send()方法,但尚未接收到響應。
- 3:接收。已經接收到部分響應資料。
- 4:完成。已經接收到全部響應資料,而且已經可以在客戶端使用了。
必須在呼叫
open()
之前指定onreadystatechange
事件處理程式才能確保跨瀏覽器相容性。(因為open()
也會觸發onreadystatechange
)// handle the server response xhr.onreadystatechange = function handleRequest () { // check the request's state if (xhr.readyState === XMLHttpRequest.DONE) { // 4 // when the response's received callback(xhr) } else { // not ready yet } } 複製程式碼
-
傳送HTTP請求
// actually make the request if (requestMethod === 'POST') { xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') xhr.send(data) } else { xhr.send() } 複製程式碼
-
獲取非同步呼叫返回的資料
用回撥的方式傳入api:
function callback (res) { // check the HTTP response status codes of the HTTP response if (res.status >= 200 && res.status < 300 || res.status === 304) { // perfect! console.log('success') ajxDisplay.innerText = res.responseText } else { // something wrong console.log('fail') } } 複製程式碼
-
使用JavaScript和DOM實現區域性重新整理
呼叫 ajax 封裝函式。
btnAjax.onclick = function () { ajax(callback, 'GET', '/ajax', true) ajax(callback, 'POST', '/ajax', true, encodeURIComponent('name') + '=' + encodeURIComponent('hi')) } 複製程式碼
IE相容性
參考:
- Msxml2.XMLHTTP和Microsoft.XMLHTTP有什麼區別?
- best method of instantiating an xmlhttprequest object
- JavaScript: Which should I use, Microsoft.XMLHTTP or Msxml2.XMLHTTP?
- Using the right version of MSXML in Internet Explorer
看到有兩種不同寫法:
// MDN
new ActiveXObject('Microsoft.XMLHTTP')
// 高程三
new ActiveXObject('MSXML2.XMLHttp.6.0')
new ActiveXObject('MSXML2.XMLHttp.3.0')
new ActiveXObject('MSXML2.XMLHttp')
複製程式碼
- Msxml2.XMLHTTP是高版本,受msxml3.dll+支援
- Microsoft.XMLHTTP是低本,一般是msxml2.6以下版本使用
[[0], 1].reduce((p, v) => p.push([v])).toString()
的值
題目:
[[0], 1].reduce((p, v) => p.push([v])).toString() // "2"
複製程式碼
Why?
基礎知識點:
-
arr.reduce(callback[, initialValue])
reduce返回值,是用的callback的返回值。
callback的四個引數:
- Accumulator (acc)
- Current Value (cur)
- Current Index (idx)
- Source Array (src)
-
push:
push的返回值,是陣列的新長度。
-
函式返回值:
具體原因不清楚。
函式返回的數值,是number的包裝物件,而非原始資料型別,所以能用
toString()
而不會報錯。function foo () { return 1; } foo().__proto__; // Number {0, constructor: ƒ, toExponential: ƒ, toFixed: ƒ, toPrecision: ƒ, …} 1.__proto__; // Uncaught SyntaxError: Invalid or unexpected token 複製程式碼
題目分析:
- 預設沒有initialValue的情況下,
p
的初始值為[0]
,v
為1
。 p.push([v])
,即p.push([1])
,於是p的現值為[0, [1]]
。- reduce返回的值 為
p.puh([1])
的返回值,即陣列的新長度,也就是2。