20181204——阮一峰閱讀 物件處理 建構函式和new命令
Array物件
push(),pop()
push方法用於在陣列的末端新增一個或多個元素,並返回新增新元素後的陣列長度。注意,該方法會改變原陣列。
var arr = [];
arr.push(1) // 1
arr.push('a') // 2
arr.push(true, {}) // 4
arr // [1, 'a', true, {}]
shift(),unshift()
shift方法用於刪除陣列的第一個元素,並返回該元素。注意,該方法會改變原陣列。
var a = ['a', 'b', 'c'];
a.shift() // 'a'
a // ['b', 'c']
join()
join方法以指定引數作為分隔符,將所有陣列成員連線為一個字串返回。如果不提供引數,預設用逗號分隔。
var a = [1, 2, 3, 4];
a.join(' ') // '1 2 3 4'
a.join(' | ') // "1 | 2 | 3 | 4"
a.join() // "1,2,3,4"
concat()
concat方法用於多個數組的合併。它將新陣列的成員,新增到原陣列成員的後部,然後返回一個新陣列,原陣列不變。
['hello'].concat(['world']) // ["hello", "world"] ['hello'].concat(['world'], ['!']) // ["hello", "world", "!"] [].concat({a: 1}, {b: 2}) // [{ a: 1 }, { b: 2 }] [2].concat({a: 1}) // [2, {a: 1}]
reverse()
reverse方法用於顛倒排列陣列元素,返回改變後的陣列。注意,該方法將改變原陣列。
var a = ['a', 'b', 'c'];
a.reverse() // ["c", "b", "a"]
a // ["c", "b", "a"]
slice()
slice方法用於提取目標陣列的一部分,返回一個新陣列,原陣列不變。
它的第一個引數為起始位置(從0開始),第二個引數為終止位置(但該位置的元素本身不包括在內)。如果省略第二個引數,則一直返回到原陣列的最後一個成員。
var a = ['a', 'b', 'c']; a.slice(0) // ["a", "b", "c"] a.slice(1) // ["b", "c"] a.slice(1, 2) // ["b"] a.slice(2, 6) // ["c"] a.slice() // ["a", "b", "c"]
splice()
splice方法用於刪除原陣列的一部分成員,並可以在刪除的位置新增新的陣列成員,返回值是被刪除的元素。注意,該方法會改變原陣列。
arr.splice(start, count, addElement1, addElement2, ...);
sort()
sort方法對陣列成員進行排序,預設是按照字典順序排序。排序後,原陣列將被改變。
['d', 'c', 'b', 'a'].sort()
// ['a', 'b', 'c', 'd']
[4, 3, 2, 1].sort()
// [1, 2, 3, 4]
[11, 101].sort()
// [101, 11]
[10111, 1101, 111].sort()
// [10111, 1101, 111]
map()
var numbers = [1, 2, 3];
numbers.map(function (n) {
return n + 1;
});
// [2, 3, 4]
numbers
// [1, 2, 3]
filter()
filter方法用於過濾陣列成員,滿足條件的成員組成一個新陣列返回。
它的引數是一個函式,所有陣列成員依次執行該函式,返回結果為true的成員組成一個新陣列返回。該方法不會改變原陣列。
[1, 2, 3, 4, 5].filter(function (elem) {
return (elem > 3);
})
// [4, 5]
some(),every()
這兩個方法類似“斷言”(assert),返回一個布林值,表示判斷陣列成員是否符合某種條件。
它們接受一個函式作為引數,所有陣列成員依次執行該函式。該函式接受三個引數:當前成員、當前位置和整個陣列,然後返回一個布林值。
some方法是隻要一個成員的返回值是true,則整個some方法的返回值就是true,否則返回false。
var arr = [1, 2, 3, 4, 5];
arr.some(function (elem, index, arr) {
return elem >= 3;
});
// true
上面程式碼中,如果陣列arr有一個成員大於等於3,some方法就返回true。
every方法是所有成員的返回值都是true,整個every方法才返回true,否則返回false。
var arr = [1, 2, 3, 4, 5];
arr.every(function (elem, index, arr) {
return elem >= 3;
});
// false
上面程式碼中,陣列arr並非所有成員大於等於3,所以返回false。
注意,對於空陣列,some方法返回false,every方法返回true,回撥函式都不會執行。
function isEven(x) { return x % 2 === 0 }
[].some(isEven) // false
[].every(isEven) // true
indexOf(),lastIndexOf()
indexOf方法返回給定元素在陣列中第一次出現的位置,如果沒有出現則返回-1。
var a = ['a', 'b', 'c'];
a.indexOf('b') // 1
a.indexOf('y') // -1
indexOf方法還可以接受第二個引數,表示搜尋的開始位置。
['a', 'b', 'c'].indexOf('a', 1) // -1
物件是什麼
面向物件程式設計(Object Oriented Programming,縮寫為 OOP)是目前主流的程式設計正規化。它將真實世界各種複雜的關係,抽象為一個個物件,然後由物件之間的分工與合作,完成對真實世界的模擬。
每一個物件都是功能中心,具有明確分工,可以完成接受資訊、處理資料、發出資訊等任務。物件可以複用,通過繼承機制還可以定製。因此,面向物件程式設計具有靈活、程式碼可複用、高度模組化等特點,容易維護和開發,比起由一系列函式或指令組成的傳統的程序式程式設計(procedural programming),更適合多人合作的大型軟體專案。
那麼,“物件”(object)到底是什麼?我們從兩個層次來理解
建構函式
面向物件程式設計的第一步,就是要生成物件。前面說過,物件是單個實物的抽象。通常需要一個模板,表示某一類實物的共同特徵,然後物件根據這個模板生成。
典型的面向物件程式語言(比如 C++ 和 Java),都有“類”(class)這個概念。所謂“類”就是物件的模板,物件就是“類”的例項。但是,JavaScript 語言的物件體系,不是基於“類”的,而是基於建構函式(constructor)和原型鏈(prototype)。
JavaScript 語言使用建構函式(constructor)作為物件的模板。所謂”建構函式”,就是專門用來生成例項物件的函式。它就是物件的模板,描述例項物件的基本結構。一個建構函式,可以生成多個例項物件,這些例項物件都有相同的結構。
建構函式就是一個普通的函式,但是有自己的特徵和用法。
new 命令
new命令的作用,就是執行建構函式,返回一個例項物件。
var Vehicle = function () {
this.price = 1000;
};
var v = new Vehicle();
v.price // 1000
上面程式碼通過new命令,讓建構函式Vehicle生成一個例項物件,儲存在變數v中。這個新生成的例項物件,從建構函式Vehicle得到了price屬性。new命令執行時,建構函式內部的this,就代表了新生成的例項物件,this.price表示例項物件有一個price屬性,值是1000。
使用new命令時,根據需要,建構函式也可以接受引數。
new 命令的原理
使用new命令時,它後面的函式依次執行下面的步驟。
建立一個空物件,作為將要返回的物件例項。
將這個空物件的原型,指向建構函式的prototype屬性。
將這個空物件賦值給函式內部的this關鍵字。
開始執行建構函式內部的程式碼。
也就是說,建構函式內部,this指的是一個新生成的空物件,所有針對this的操作,都會發生在這個空物件上。建構函式之所以叫“建構函式”,就是說這個函式的目的,就是操作一個空物件(即this物件),將其“構造”為需要的樣子。
如果建構函式內部有return語句,而且return後面跟著一個物件,new命令會返回return語句指定的物件;否則,就會不管return語句,返回this物件。
上面程式碼中,建構函式Vehicle的return語句返回一個數值。這時,new命令就會忽略這個return語句,返回“構造”後的this物件。
但是,如果return語句返回的是一個跟this無關的新物件,new命令會返回這個新物件,而不是this物件。這一點需要特別引起注意。
var Vehicle = function (){
this.price = 1000;
return { price: 2000 };
};
(new Vehicle()).price
// 2000
this關鍵字
this關鍵字是一個非常重要的語法點。毫不誇張地說,不理解它的含義,大部分開發任務都無法完成。
前一章已經提到,this可以用在建構函式之中,表示例項物件。除此之外,this還可以用在別的場合。但不管是什麼場合,this都有一個共同點:它總是返回一個物件。
簡單說,this就是屬性或方法“當前”所在的物件。
var person = {
name: '張三',
describe: function () {
return '姓名:'+ this.name;
}
};
person.describe()
// "姓名:張三"
上面程式碼中,this.name表示name屬性所在的那個物件。由於this.name是在describe方法中呼叫,而describe方法所在的當前物件是person,因此this指向person,this.name就是person.name。
由於物件的屬性可以賦給另一個物件,所以屬性所在的當前物件是可變的,即this的指向是可變的。
(1)全域性環境
全域性環境使用this,它指的就是頂層物件window。
this === window // true
function f() {
console.log(this === window);
}
f() // true
上面程式碼說明,不管是不是在函式內部,只要是在全域性環境下執行,this就是指頂層物件window。
(2)建構函式
建構函式中的this,指的是例項物件。
var Obj = function (p) {
this.p = p;
};
(3)物件的方法
如果物件的方法裡面包含this,this的指向就是方法執行時所在的物件。該方法賦值給另一個物件,就會改變this的指向。
但是,這條規則很不容易把握。請看下面的程式碼。
var obj ={
foo: function () {
console.log(this);
}
};
obj.foo() // obj
使用注意點
避免多層 this
由於this的指向是不確定的,所以切勿在函式中包含多層的this
var o = {
f1: function () {
console.log(this);
var f2 = function () {
console.log(this);
}();
}
}
o.f1()
// Object
// Window
上面程式碼包含兩層this,結果執行後,第一層指向物件o,第二層指向全域性物件,因為實際執行的是下面的程式碼。
var temp = function () {
console.log(this);
};
var o = {
f1: function () {
console.log(this);
var f2 = temp();
}
}
一個解決方法是在第二層改用一個指向外層this的變數。
var o = {
f1: function() {
console.log(this);
var that = this;
var f2 = function() {
console.log(that);
}();
}
}
o.f1()
// Object
// Object
上面程式碼定義了變數that,固定指向外層的this,然後在內層使用that,就不會發生this指向的改變。
事實上,使用一個變數固定this的值,然後內層函式呼叫這個變數,是非常常見的做法,請務必掌握。
JavaScript 提供了嚴格模式,也可以硬性避免這種問題。嚴格模式下,如果函式內部的this指向頂層物件,就會報錯。
繫結 this 的方法
this的動態切換,固然為 JavaScript 創造了巨大的靈活性,但也使得程式設計變得困難和模糊。有時,需要把this固定下來,避免出現意想不到的情況。JavaScript 提供了call、apply、bind這三個方法,來切換/固定this的指向。
Function.prototype.call()
函式例項的call方法,可以指定函式內部this的指向(即函式執行時所在的作用域),然後在所指定的作用域中,呼叫該函式。
var obj = {};
var f = function () {
return this;
};
f() === window // true
f.call(obj) === obj // true
上面程式碼中,全域性環境執行函式f時,this指向全域性環境(瀏覽器為window物件);call方法可以改變this的指向,指定this指向物件obj,然後在物件obj的作用域中執行函式f。
call方法的引數,應該是一個物件。如果引數為空、null和undefined,則預設傳入全域性物件。
var n = 123;
var obj = { n: 456 };
function a() {
console.log(this.n);
}
a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456
上面程式碼中,a函式中的this關鍵字,如果指向全域性物件,返回結果為123。如果使用call方法將this關鍵字指向obj物件,返回結果為456。可以看到,如果call方法沒有引數,或者引數為null或undefined,則等同於指向全域性物件。
Function.prototype.apply()
apply方法的作用與call方法類似,也是改變this指向,然後再呼叫該函式。唯一的區別就是,它接收一個數組作為函式執行時的引數,使用格式如下。
Function.prototype.bind()
原型物件概述
JavaScript 通過建構函式生成新物件,因此建構函式可以視為物件的模板。例項物件的屬性和方法,可以定義在建構函式內部。
function Cat (name, color) {
this.name = name;
this.color = color;
}
var cat1 = new Cat('大毛', '白色');
cat1.name // '大毛'
cat1.color // '白色'
上面程式碼中,cat1和cat2是同一個建構函式的兩個例項,它們都具有meow方法。由於meow方法是生成在每個例項物件上面,所以兩個例項就生成了兩次。也就是說,每新建一個例項,就會新建一個meow方法。這既沒有必要,又浪費系統資源,因為所有meow方法都是同樣的行為,完全應該共享。
prototype 屬性的作用
JavaScript 繼承機制的設計思想就是,原型物件的所有屬性和方法,都能被例項物件共享。也就是說,如果屬性和方法定義在原型上,那麼所有例項物件就能共享,不僅節省了記憶體,還體現了例項物件之間的聯絡。
原型鏈
JavaScript 規定,所有物件都有自己的原型物件(prototype)。一方面,任何一個物件,都可以充當其他物件的原型;另一方面,由於原型物件也是物件,所以它也有自己的原型。因此,就會形成一個“原型鏈”(prototype chain):物件到原型,再到原型的原型……
如果一層層地上溯,所有物件的原型最終都可以上溯到Object.prototype,即Object建構函式的prototype屬性。也就是說,所有物件都繼承了Object.prototype的屬性。這就是所有物件都有valueOf和toString方法的原因,因為這是從Object.prototype繼承的。
那麼,Object.prototype物件有沒有它的原型呢?**回答是Object.prototype的原型是null。**null沒有任何屬性和方法,也沒有自己的原型。因此,原型鏈的盡頭就是null。