前端進階-類和物件

moduzhang發表於2018-11-16

物件可以表示資料和功能。

建構函式

建構函式:結構和語法

function SoftwareDeveloper() {
  this.favoriteLanguage = 'JavaScript';
}

首先,建構函式並不宣告區域性變數,而是使用 this 關鍵字來儲存資料。以上函式將為所建立的任何物件新增一個 favoriteLanguage 屬性,併為其分配一個預設值 'JavaScript'。現在不用太在意建構函式中的 this;只要知道 this 是指在建構函式前面使用 new 關鍵字建立的新物件即可。

最後一點比較特別的是,這個函式似乎不會返回任何東西!JavaScript 中的建構函式不應該有一個顯式的返回值(即使用 return 語句)。

建立一個新的物件

function SoftwareDeveloper() {
	this.favoriteLanguage = 'JavaScript';
}
let developer = new SoftwareDeveloper();
console.log(developer);
// SoftwareDeveloper {this.favoriteLanguage: "JavaScript"}

function SoftwareDeveloper(name) {
  this.favoriteLanguage = 'JavaScript';
  this.name = name;
}
let instructor = new SoftwareDeveloper('Andrew');
console.log(instructor);
// SoftwareDeveloper { favoriteLanguage: 'JavaScript', name: 'Andrew' }

注意,大寫建構函式名稱的第一個字母只是一個命名慣例。雖然第一個字母應該大寫,但如果不小心用了小寫,也仍然還是建構函式(即當用 new 運算子呼叫時,等等)。

檢視物件的建構函式 (instanceOf)

function Developer(name) {
	this.name = name;
}
const dev = new Developer('Veronika');
typeof dev // "object"
dev instanceOf Developer; // true 

new 運算子
instanceOf 運算子

當在物件上呼叫方法時,this 會被賦值,並且其值指向該物件。由於它是一個保留字,因此不應該用作任何變數名稱、函式名稱等。

關鍵字 this

根據 this 的呼叫方式,分析 this 指代的含義:

在這裡插入圖片描述

如果使用 new 運算子來呼叫建構函式,this 的值將被設定為新建立的物件。如果在物件上呼叫方法,this 將被設定為該物件本身。如果簡單地呼叫一個函式,this 將被設定為全域性物件:window

this 運算子

設定自己的 this

Javascript 提供了幾種設定 this 值的方法,這些方法分別是 call()apply()bind()。前兩種方法在函式上被呼叫,會因為引數的傳入方式不同而有所不同。第三種方法是返回新函式的方法,每種方法都在不同的環境中使用。

call()

call() 是一個直接呼叫到函式上的方法。我們傳遞給它一個單一的值,以設定為 this 的值,然後逐個傳入該函式的任何引數,用逗號分隔。

function multiply(n1, n2) {
  return n1 * n2;
}
multiply(3, 4);
// 12
multiply.call(window, 3, 4);
// 12

除了呼叫常規函式之外,我們如何呼叫附加到物件上的函式(即方法)呢?

使用 call() 來呼叫方法允許我們從物件中“借用”方法,然後將其用於另一個物件

const mockingbird = {
  title: 'To Kill a Mockingbird',
  describe: function () {
    console.log(`${this.title} is a classic novel`);
  }
};

mockingbird.describe();
// 'To Kill a Mockingbird is a classic novel'

const pride = {
  title: 'Pride and Prejudice'
};
mockingbird.describe.call(pride);
// 'Pride and Prejudice is a classic novel'

首先,call() 方法被呼叫到 mockingbird.describe(它指向一個函式)上。然後,this 的值被傳遞給 call() 方法:pride

由於 mockingbirddescribe() 方法引用了 this.title,我們需要訪問 this 所指向的物件的 title 屬性。但是,由於我們已經設定了自己的 this 的值,this.title 的值將會從 pride 物件中被訪問!結果,mockingbird.describe.call(pride); 被執行,我們在控制檯中看到 'Pride and Prejudice is a classic novel'

apply()

multiply.apply(window, [3, 4]); // 將函式的引數放在一個陣列中
// 12

就像 call() 一樣,apply() 在一個函式上被呼叫,不僅可以呼叫該函式,而且還可以為它關聯一個特定的 this 值。但是,apply() 並不是逐個傳遞引數並用逗號分隔,而是將函式的引數放在一個陣列中

mockingbird.describe.apply(pride);
// 'Pride and Prejudice is a classic novel'

傳遞給 call()apply() 的第一個引數是相同的(即繫結 this 值的物件)。由於 describe() 方法不接受任何引數,因此 mockingbird.describe.call(pride);mockingbird.describe.apply(pride); 唯一的區別就是方法!這兩種方法都會產生相同的結果。

偏向選擇其中一種方法

如果你事先並不知道函式所需要的引數個數,那麼 call() 的使用可能會受到限制。在這種情況下,apply() 是一個更好的選擇,因為它只接受一個引數陣列,然後將其解包並傳遞給函式。請記住,解包可能會略微影響效能,但這種影響並不顯著。

回撥和 this

function invokeTwice(cb) {
	cb();
	cb();
}

const dog = {
	age: 5,
	growOneYear: function () {
		this.age += 1;
	}
}

dog.growOneYear();
dog.age; // 6

invokeTwice(dog.growOneYear); //函式來呼叫它,因此 this 設成了全域性變數,而不是 dog 物件
dog.age; // 6 , dog 的 age 屬性沒有發生變化

使用匿名閉包來儲存 this

簡單地呼叫一個普通函式會將 this 的值設定為全域性物件(即 window)。我們如何解決這個問題呢?

解決這個問題的一種方式就是使用一個匿名閉包來遮蔽 dog 物件:

invokeTwice(function () { 
  dog.growOneYear(); 
});

dog.age
// 7

使用 bind() 來儲存 this

call()apply() 類似,bind() 方法也允許使用者直接為 this 定義一個值。bind() 也是一個在函式上呼叫的方法,但不同於 call()apply(),它們都會立即呼叫函式——bind()返回一個新的函式。當被呼叫時,該函式會將 this 設定為我們賦給它的值。

function invokeTwice(cb) {
	cb();
	cb();
}

const dog = {
	age: 5,
	growOneYear: function () {
		this.age += 1;
	}
}

invokeTwice(dog.growOneYear);
const myGrow = dog.growOneYear.bind(dog);
invokeTwice(myGrow);
dog.age; // 7

bind()可以直接在函式中呼叫的方法,返回該函式的副本,並具有特定的 this 值。

call()
apply()
bind()

原型繼承

在這裡插入圖片描述

Cat() 建構函式是使用 new 運算子來呼叫的,該運算子建立了 bailey 例項(物件)。請注意,meow() 方法是在 bailey 物件的建構函式的原型中定義的。原型只是一個物件,該建構函式所建立的所有物件均被祕密連結到該原型。因此,我們可以將 bailey.meow() 當作 bailey 自身的方法執行。

無論你是訪問屬性(例如 bailey.lives;)還是呼叫方法(即 bailey.meow();),JavaScript 直譯器都會按照特定的順序在原型鏈中查詢它們:

  1. 首先,JavaScript 引擎將檢視物件自身的屬性。這意味著,直接在該物件中定義的任何屬性和方法將優先於其他位置的任何同名屬性和方法(類似於作用域鏈中的變數陰影)。
  2. 如果找不到目標屬性,它將搜尋物件的建構函式的原型,以尋找匹配。
  3. 如果原型中不存在該屬性,則 JavaScript 引擎將沿著該鏈繼續查詢。
  4. 該鏈的最頂端是 Object() 物件,也就是頂級父物件。如果_仍然_找不到該屬性,則該屬性為未定義。

每個函式都有一個 prototype 屬性,它其實只是一個物件。當使用 new 運算子將該函式作為建構函式來呼叫時,它會建立並返回一個新的物件。該物件被祕密地連結到其建構函式的 prototype,而這個祕密連結讓該物件可以訪問 prototype 的屬性和方法,就像它自己的一樣!

function Dog(age, weight, name) {
	this.age = age;
	this.weight = weight;
	this.name = name;
	/**
	this.bark = function() {
		console.log(`${this.name} says woof!`);
	};
	*/
}
// 在 dog 原型中定義 bark
Dog.prototype.bark = function() {
	console.log(`${this.name} says woof!`);
};
dog = new Dog(2, 60, 'Java');
dog.bark(); // Java says woof!

當我們在 Dog 中叫 bark 方法時,JavaScript 引擎會檢視自己的屬性,嘗試找到與 bark 方法相匹配的名稱,由於 bark 沒有直接定義在這個 dog 上,它會看看 bark 方法的原型,最後我們不需要呼叫 dog.prototype.bark,只需要呼叫 dog.bark 就可以了。因為這個 dog 物件已經通過它的原型與 bark 方法聯絡起來。

替換 prototype 物件

function Hamster() { this.hasFur = true; }
let waffle = new Hamster();
let pancake = new Hamster(); 

在建立新的物件 wafflepancake 之後,我們仍然可以為 Hamster 的原型新增屬性,而且它仍然可以訪問這些新的屬性。

Hamster.prototype.eat = function () { console.log('Chomp chomp chomp!'); };
waffle.eat(); // 'Chomp chomp chomp!'
pancake.eat(); // 'Chomp chomp chomp!'

我們將 Hamsterprototype 物件完全替換為其他內容:

Hamster.prototype = {
  isHungry: false,
  color: 'brown'
};

先前的物件無法訪問更新後的原型的屬性;它們只會保留與舊原型的祕密連結:

console.log(waffle.color); // undefined
waffle.eat(); // 'Chomp chomp chomp!'
console.log(pancake.isHungry); // undefined 

事實證明,此後建立的任何新的 Hamster 物件都會使用更新後的原型:

const muffin = new Hamster();
muffin.eat(); // TypeError: muffin.eat is not a function
console.log(muffin.isHungry); // false
console.log(muffin.color); // 'brown'

檢查物件的屬性

hasOwnProperty()

hasOwnProperty() 可以幫助你找到某個特定屬性的來源。在向其傳入你要查詢的屬性名稱的字串後,該方法會返回一個布林值,指示該屬性是否屬於該物件本身(即該屬性不是被繼承的)。

function Phone() {
   this.operatingSystem = 'Android';
}
Phone.prototype.screenSize = 6;

const myPhone = new Phone();
const own = myPhone.hasOwnProperty('operatingSystem');
console.log(own);
//true

const inherited = myPhone.hasOwnProperty('screenSize');
console.log(inherited);
//false

isPrototypeOf()

物件還可以訪問 isPrototypeOf() 方法,該方法可以檢查某個物件是否存在於另一個物件的原型鏈中。 使用這種方法,你可以確認某個特定的物件是否是另一個物件的原型

const rodent = {
   favoriteFood: 'cheese',
   hasTail: true
};

function Mouse() {
   this.favoriteFood = 'cheese';
}
Mouse.prototype = rodent;

const ralph = new Mouse();
const result = rodent.isPrototypeOf(ralph)
console.log(result);
//true

建立一個新的 Mouse 物件,它的原型應該是 rodent 物件。isPrototypeOf() 是確認某個物件是否存在於另一個物件的原型鏈中的好辦法。

Object.getPrototypeOf()

const myPrototype = Object.getPrototypeOf(ralph);
console.log(myPrototype);
//{ favoriteFood: "cheese", hasTail: true }

ralph 的原型與結果具有相同的屬性,因為它們就是同一個物件。 Object.getPrototypeOf() 很適合檢索給定物件的原型

const capitals = {
  California: 'Sacramento',
  Washington: 'Olympia',
  Oregon: 'Salem',
  Texas: 'Austin'
};
Object.getPrototypeOf(capitals) === Object.prototype
// true

物件字面量表示法建立的物件,它的建構函式就是內建的 Object() 建構函式本身!因此,它會保持一個對其建構函式原型的引用

  • isPrototypeOf() 可以檢查某個物件是否存在於另一個物件的原型鏈中
  • isPrototypeOf() 會接受一個引數,一個原型鏈將被搜尋的物件
  • getPrototypeOf() 會返回傳遞給它的物件的原型

constructor 屬性

每次建立一個物件時,都會有一個特殊的屬性被暗中分配給它:constructor。訪問一個物件的 constructor 屬性會返回一個對建立該物件的建構函式的引用

function Longboard() {
   this.material = 'bamboo';
}
const board = new Longboard();
console.log(board.constructor);
//function Longboard() {
//   this.material = 'bamboo';
//}

如果某個物件是使用字面量表示法建立的,那麼它的建構函式就是內建的 Object() 建構函式!

const rodent = {
   teeth: 'incisors',
   hasTail: true
};
console.log(rodent.constructor);
//function Object() { [native code] }
  • 訪問一個物件的 constructor 屬性會返回一個對建立該物件(例項)的建構函式的引用
  • 每個物件都有一個 constructor 屬性
  • 使用字面量表示法建立的物件是用 Object() 建構函式建立的

hasOwnProperty()
isPrototypeOf()
Object.getPrototypeOf()
.constructor

相關文章