1. 程式人生 > >20181204——阮一峰閱讀 物件處理 建構函式和new命令

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。