《JavaScript語言入門教程》記錄整理:面向物件
阿新 • • 發佈:2020-08-12
[toc]
本系列基於阮一峰老師的[《JavaScrip語言入門教程》](https://wangdoc.com/javascript/index.html)或《JavaScript教程》記錄整理,教程採用[知識共享 署名-相同方式共享 3.0協議](https://creativecommons.org/licenses/by-sa/3.0/deed.zh)。這幾乎是學習js最好的教程之一(去掉之一都不過分)
最好的教程而阮一峰老師又採用開源方式共享出來,之所以重新記錄一遍,一是強迫自己重新認真讀一遍學一遍;二是對其中知識點有個自己的記錄,加深自己的理解;三是感謝這麼好的教程,希望更多人閱讀了解
# 面向物件程式設計
## 例項物件與 new 命令
1. 面向物件程式設計(`Object Oriented Programming`,`OOP`)將現實世界中的實物、邏輯操作及各種複雜關係抽象為一個個物件,每一個物件完成一定的功能,用來接受資訊、處理資料或執行操作、釋出資訊等,通過繼承還能實現複用和功能擴充套件。比起由一系列函式或指令組成的傳統的程序式程式設計(`procedural programming`)更適合大型專案。
2. 什麼是"物件"(`object`):(1)物件是單個實物的抽象。(2)物件是一個容器,封裝了屬性(property)和方法(method)。屬性是物件的狀態,方法是物件的行為(完成某種任務)。
3. 生成物件時,通常需要一個模板,表示某一類實物的共同特徵,然後根據模板生成。在C++、java、c#等語言中都有類(class)的概念。"類"就是物件的模板,物件是"類"的例項(即類的一個具體物件)。JavaScript的物件體系基於建構函式(`constructor`)和原型鏈(`prototype`)構成。
4. JavaScript 語言中建構函式(`constructor`)就是物件的模板,描述例項物件的基本結構。"建構函式"就是專門用來生成例項物件的函式。一個建構函式,可以生成多個例項物件,這些例項物件都有相同的結構。
5. 建構函式和普通函式一樣,但是有自己的特徵和用法。
如下,`Vehicle`就是建構函式。通常建構函式名字第一個字母大寫(與普通函式作區分)。
```js
var Vehicle = function () {
this.price = 1000;
};
```
**建構函式的特點**:
- 函式體內部使用了`this`關鍵字,代表了所要生成的物件例項。
- 生成物件的時候,必須使用`new`命令。
6. `new`命令的作用是執行建構函式,返回一個例項物件。
```js
var Vehicle = function () {
this.price = 1000;
};
var v = new Vehicle();
v.price // 1000
```
**如果忘記了new命令,就成了建構函式作為普通函式直接呼叫**
為了保證建構函式必須使用new命令,解決辦法有兩種:
一、可以在建構函式內部使用嚴格模式。這樣不使用new命令直接呼叫就會報錯
```js
var Vehicle = function () {
'use strict';
this.price = 1000;
};
var v = Vehicle(); // Uncaught TypeError: Cannot set property 'price' of undefined
```
> 嚴格模式中,函式內部的`this`不能指向全域性物件,預設等於`undefined`,導致不加`new`呼叫會報錯
二、在建構函式內部判斷是否使用`new`命令,如果沒有,則根據引數返回一個例項物件。
```js
function Vehicle(price) {
if (!(this instanceof Vehicle)) {
return new Vehicle(price);
}
this.price = price||1000;
};
var v1 = Vehicle();
var v2 = new Vehicle();
```
7. 使用`new`命令時,後面的函式依次執行下面的步驟。
- 建立一個空物件,作為將要返回的物件例項。
- 將這個空物件的原型,指向建構函式的`prototype`屬性。
- 將這個空物件賦值給函式內部的this關鍵字。
- 開始執行建構函式內部的程式碼。
建構函式內部,`this`指的是一個新生成的空物件。建構函式的目的就是操作一個空物件(即`this`物件),將其"構造"為需要的樣子。
> 如果建構函式內部有return語句且return後面跟著一個物件,new命令會返回return語句指定的物件;否則,就會不管return語句,返回this物件。
> ```js
> var Vehicle = function () {
> this.price = 1000;
> return 1000; // 忽略非物件的return語句
> };
>
> (new Vehicle()) === 1000
> ```
>
> 如果return返回的是其他物件而不是this,那麼new命令將會返回這個新物件
>
> 如果對普通函式(內部沒有this關鍵字的函式)使用new命令,則會返回一個空物件。
> ```js
> function getMessage() {
> return 'this is a message';
> }
>
> var msg = new getMessage();
> msg // {}
> typeof msg // "object"
> ```
> `new`命令簡化的內部流程,可用下面的程式碼表示。
> ```js
> function _new(/* 建構函式 */ constructor, /* 建構函式引數 */ params) {
> // 將 arguments 物件轉為陣列
> var args = [].slice.call(arguments);
> // 取出建構函式
> var constructor = args.shift();
> // 建立一個空物件,繼承建構函式的 prototype 屬性
> var context = Object.create(constructor.prototype);
> // 執行建構函式
> var result = constructor.apply(context, args);
> // 如果返回結果是物件,就直接返回,否則返回 context 物件
> return (typeof result === 'object' && result != null) ? result : context;
> }
>
> // 例項
> var actor = _new(Person, '張三', 28);
> ```
8. 函式內部的`new.target`屬性。如果當前函式是new命令呼叫,`new.target`指向當前函式,否則為`undefined`。
```js
function f() {
console.log(new.target === f);
}
f() // false
new f() // true
```
此屬性可判斷是否使用new命令呼叫了函式
```js
function f() {
if (!new.target) {
throw new Error('請使用 new 命令呼叫!');
}
// ...
}
f() // Uncaught Error: 請使用 new 命令呼叫!
```
9. `Object.create()` 建立例項物件
通常使用建構函式作為生成例項物件的模板。但是如果沒有建構函式只有物件時,可以使用`Object.create()`方法以一個物件作為模板,生成新的例項物件。
如下,物件`person1`是`person2`的模板,後者繼承了前者的屬性和方法。
```js
var person1 = {
name: '張三',
age: 38,
greeting: function() {
console.log('你好,我是' + this.name + '。');
}
};
var person2 = Object.create(person1);
person2.name; // "張三"
person2.name="李四" // "李四"
person2.greeting() // 你好,我是李四。
person1.greeting() // 你好,我是張三。
```
## this關鍵字
1. `this`關鍵字總是返回一個物件,或指向一個物件。
2. `this`就是屬性或方法"當前"所在的物件。也就是說,如果改變屬性或方法所在的物件,就可以改變this的指向
將物件的屬性賦給另一個物件,改變屬性所在物件,可以改變this的指向。
如下,通過改變函式`f`所在的物件,實現this的改變
```js
function f() {
return '姓名:'+ this.name;
}
var A = {
name: '張三',
describe: f
};
var B = {
name: '李四',
describe: f
};
f() // "姓名:"
A.describe() // "姓名:張三"
B.describe() // "姓名:李四"
```
**只要函式被賦給另一個變數,this的指向就會變。**
4. JavaScript中,一切皆物件。執行環境也是物件(頂層函式中,this指向window物件),函式都是在某個物件之中執行,`this`就是函式執行時所在的物件(環境)。同時this的指向是動態的
5. `this`的本質或`this`的設計目的:
js的物件在記憶體的結構是這樣的,物件存在堆中,當把物件賦值給一個變數時,實際是將物件在堆中的記憶體地址賦值給變數。如下,將物件的地址(`reference`)賦值給變數obj
```js
var obj = { foo: 5 };
```
讀取`obj.foo`的過程是,先從obj拿到記憶體地址,然後從該地址讀出原始的物件,返回它的`foo`屬性
原始的物件以字典結構儲存,每一個屬性名都對應一個屬性描述物件。比如上面的屬性`foo`實際儲存形式如下,`foo`屬性的值儲存在屬性描述物件的`value`屬性裡面:
```js
{
foo: {
[[value]]: 5
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
```
當屬性的值是函式時
```js
var obj = { foo: function () {} };
```
js將函式單獨儲存在記憶體中,將函式的地址賦值給`foo`屬性的`value`屬性。
```js
{
foo: {
[[value]]: 函式的地址
...
}
}
```
因為函式是單獨存在的值,所以可以在不同的環境(上下文)執行
JavaScript允許在函式體內部,引用當前環境的其他變數。
如下,函式體使用的變數x由執行環境提供。
```js
var f = function () {
console.log(x);
};
```
由於函式可以在不同的執行環境執行,所以需要一種機制,可以**在函式體內部獲得當前的執行環境(context)**。所以`this`就被用來設計為,**在函式體內部,指代函式當前的執行環境**。
如下,函式體中`this.x`就指當前執行環境的`x`。
```js
var f = function () {
console.log(this.x);
}
```
6. `this`的使用場合
- 全域性環境使用`this`,指的是頂層物件`window`。
- 建構函式中的`this`,指的是例項物件。
- 物件的方法裡面包含`this`,`this`的指向就是方法執行時所在的物件。該方法賦值給另一個物件,會改變`this`的指向。
關於`this`的指向並不好把握,比如下面的例子
```js
var obj ={
foo: function () {
console.log(this);
}
};
obj.foo() // obj
```
如上,通過呼叫boj物件的foo方法,輸出this為當前的obj物件。但是,如果使用下面的形式,都會改變this的指向
```js
// 情況一
(obj.foo = obj.foo)() // window
// 情況二
(false || obj.foo)() // window
// 情況三
(1, obj.foo)() // window
```
上面程式碼中,`obj.foo`是獲取出來之後再呼叫,相當於一個值,這個值在呼叫的時候,執行環境已經從`obj`變為了全域性環境,`this`的指向變為了`window`
可以這樣理解,在js引擎內部,`obj`物件和`obj.foo`函式儲存在兩個記憶體地址,稱為地址一和地址二。`obj.foo()`呼叫時,是從地址一呼叫地址二,因此地址二的執行環境是地址一,`this`指向`obj`。上面三種情況,都是直接取出地址二進行呼叫(即取出函式呼叫),這樣的話,執行環境就是全域性環境,`this`指向的是全域性環境。上面三種情況等同於下面的程式碼:
```js
// 情況一
(obj.foo = function () {
console.log(this);
})()
// 等同於
(function () {
console.log(this);
})()
// 情況二
(false || function () {
console.log(this);
})()
// 情況三
(1, function () {
console.log(this);
})()
```
`this`所在的方法不在物件的第一層時,這時`this`指向當前一層的物件(即當前所在的物件),而不會繼承更上面的層。
```js
var a = {
p: 'Hello',
b: {
m: function() {
console.log(this.p);
}
}
};
a.b.m() // undefined
```
7. `this`使用中注意點:
- 避免多層`this`。用於`this`的指向可變,儘量不要在函式中包含多層this
通過新增指向this的變數,實現多層this的使用
```js
var o = {
f1: function() {
console.log(this);
var that = this;
var f2 = function() {
console.log(that);
}();
}
}
o.f1()
// Object
// Object
```
JavaScript嚴格模式下,如果函式內部的`this`指向頂層物件,就會報錯。
- 避免使用陣列處理方法(`map`和`foreach`方法中的引數函式)中的`this`
`map`、`foreach`方法的回撥函式中的`this`指向window物件。解決辦法是使用一箇中間變數固定this,或者使用`this`作為`map`、`foreach`方法的第二個引數
```js
// 中間變數
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
var that = this;
this.p.forEach(function (item) {
console.log(that.v+' '+item);
});
}
}
o.f()
// hello a1
// hello a2
// 第二個引數this
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) {
console.log(this.v + ' ' + item);
}, this);
}
}
o.f()
// hello a1
// hello a2
```
- 回撥函式中避免使用`this`(往往會改變指向)。
8. `this`的動態切換,既體現了靈活,又使程式設計變得困難和模糊。js提供了`call`、`apply`、`bind`方法,來切換/固定`this`的指向。
9. `Function.prototype.call()`:**函式例項**的`call`方法,可以指定函式內部`this`的指向(即函式執行時所在的作用域),然後在指定的作用域中呼叫該函式
如下,使用call改變作用域6
```js
var obj = {};
var f = function () {
return this;
};
f() === window // true
f.call(obj) === obj // true
```
`call`方法的第一個引數,應該是一個物件。如果引數為空、`null`和`undefined`,則this指向全域性物件。
```js
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
```
`call`方法的第一個引數是一個原始值,則原始值會自動轉成對應的包裝物件,然後傳入`call`方法。
```js
var f = function () {
return this;
};
f.call(5) // Number {[[PrimitiveValue]]: 5}
```
`call`方法除第一個引數表示呼叫函式的作用域,其他引數以列表的形式傳遞,表示函式執行時的引數
```js
func.call(thisValue, arg1, arg2, ...)
```
**call方法的一個應用是呼叫物件的原生方法。**
```js
var obj = {};
obj.hasOwnProperty('toString') // false
// 覆蓋掉繼承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
return true;
};
obj.hasOwnProperty('toString') // true
Object.prototype.hasOwnProperty.call(obj, 'toString') // false
```
10. `Function.prototype.apply()`:`apply`方法的作用,也是改變`this`指向,然後再呼叫該函式。但是它接收的是一個數組作為函式執行時的引數,
```js
func.apply(thisValue, [arg1, arg2, ...])
```
和`call`一樣,第一個引數是`this`指向的物件。null或undefined表示全域性物件。第二個引數是陣列,表示傳入原函式的引數
> `apply`陣列,`call`列表
(1)找出陣列最大元素
js預設沒有找出陣列最大元素的函式,結合`apply`和`Math.max`可實現返回陣列的最大元素
```js
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15
```
(2)將陣列的空元素變為`undefined`
結合`apply`和`Array`建構函式將陣列的空元素變成`undefined`。
```js
Array.apply(null, ['a', ,'b']) // [ 'a', undefined, 'b' ]
```
> `forEach`等迴圈方法會跳過空元素,但是不會跳過`undefined`
(3)轉換類似陣列的物件
利用陣列物件的`slice`方法,可以將一個類似陣列的物件(如`arguments`物件)轉為真正的陣列。
```js
Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, 空]
Array.prototype.slice.apply({length: 1}) // [空]
```
(4)繫結回撥函式的物件
可以在事件方法等回撥函式中,通過`apply`/`call`繫結方法呼叫的物件,修改this指向
```js
var o = new Object();
o.f = function () {
console.log(this === o);
}
var f = function (){
o.f.apply(o);
// 或者 o.f.call(o);
};
// jQuery 的寫法
$('#button').on('click', f);
```
因為`apply()`/`call()`方法在繫結函式執行時所在的物件時,還會立即執行函式,因此需要把繫結語句寫在一個函式體內。
11. `Function.prototype.bind()`:`bind()`方法將函式體內的`this`繫結到某個物件,然後返回一個新函式。
如下是一個通過賦值導致函式內部this指向改變的示例。
```js
var d = new Date();
d.getTime() // 1596621203097
var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.
```
將`d.getTime`賦值給變數`print`後,方法內部的this由原來指向Date物件例項改為了window物件,`print()`執行報錯。
使用`bind()`方法繫結函式執行的this指向,可以解決這個問題。
```js
var print = d.getTime.bind(d);
undefined
print() // 1596621203097
```
`bind()`可接受更多引數,將這些引數繫結原函式的引數。
```js
var add = function (x, y) {
return x * this.m + y * this.n;
}
var obj = {
m: 2,
n: 2
};
var newAdd = add.bind(obj, 5);
newAdd(5) // 20
```
如上,`bind()`方法除了繫結`this`物件,還繫結`add()`函式的第一個引數`x`為`5`,然後返回一個新函式`newAdd()`,這個函式只要再接受一個引數`y`就能運行了。
`bind()`第一個引數是`null`或`undefined`時,`this`繫結的是全域性物件(瀏覽器環境為`window`)
12. `bind()`方法特定:
- 每一次返回一個新函式
這就導致,如果繫結事件時直接使用`bind()`會繫結為一個匿名函式,導致無法取消事件繫結
```js
element.addEventListener('click', o.m.bind(o));
// 如下取消是無效的
element.removeEventListener('click', o.m.bind(o));
```
正確寫法:
```js
var listener = o.m.bind(o);
element.addEventListener('click', listener);
// ...
element.removeEventListener('click', listener);
```
- 結合回撥函式使用。將包含`this`的方法直接當做回撥函式,會導致函式執行時改變了this的指向,從而出錯。解決辦法是使用`bind()`方法繫結回撥函式的`this`物件。當然,也可使用中間變數固定`this`
- 結合`call()`方法使用。改寫一些JS原生方法的使用
如下陣列的slice方法
```js
[1, 2, 3].slice(0, 1) // [1]
// 等同於
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
```
**`call()`方法實質上是呼叫`Function.prototype.call()`方法。**
```js
// 上面等同於
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]
```
相當於在`Array.prototype.slice`呼叫`Function.prototype.call`,引數為`(物件,slice的引數)`
類似的寫法:
```js
var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);
var a = [1 ,2 ,3];
push(a, 4)
a // [1, 2, 3, 4]
pop(a)
a // [1, 2, 3]
```
更進一步`bind`的呼叫也可以改寫:在`Function.prototype.bind`上呼叫`call`方法(返回的是一個新方法),方法引數是`(this物件,bind方法引數)`。即最終結果是在`this物件`上執行`bind`方法並傳遞引數。(有些繞)
```js
function f() {
console.log(this.v);
}
var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f, o)() // 123
```
## 物件的繼承
1. 物件的繼承可以實現程式碼的複用
2. 傳統JavaScript的繼承是通過"原型物件"(prototype)實現的。即js的原型鏈繼承。*ES6引入了class語法,實現基於class的繼承*
3. 建構函式的缺點:建構函式中通過給`this`物件的屬性賦值,可以很方便地定義例項物件屬性。但是這種方式,同一個建構函式的多個例項之間無法共享屬性。
```js
function Cat(name, color) {
this.name = name;
this.color = color;
this.features = {
species:'貓',
habits:'肉食夜行動物'
};
this.meow = function () {
console.log('喵喵');
};
}
var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');
cat1.meow === cat2.meow // false
cat1.features === cat2.features // false
```
`cat1`和`cat2`是同一個建構函式的兩個例項,因為所有`meow`方法和`features`對所有例項具有同樣的行為和屬性,應該共享而不是每個例項都建立新的方法和屬性,沒必要又浪費系統資源。
**原型物件(`prototype`)用來在例項間共享屬性。**
4. JavaScript繼承機制的設計思想:原型物件的所有屬性和方法,都能被例項物件共享
5. JavaScript規定,**每個函式都有一個`prototype`屬性,指向一個物件**。
```js
function f() {}
typeof f.prototype // "object"
```
普通函式基本不會用`prototype`屬性
建構函式生成例項的時候,建構函式的`prototype`屬性會自動成為例項物件的原型。
```js
function Cat(name, color) {
this.name = name;
}
Cat.prototype.color = 'white';
Cat.prototype.features = {
species:'貓',
habits:'肉食夜行動物'
};
Cat.prototype.meow = function () {
console.log('喵喵');
};
var cat1 = new Cat('大毛');
var cat2 = new Cat('二毛');
```
原型物件的屬性不是例項物件自身的屬性。其變動體現在所有例項物件上。
當例項物件本身沒有某個屬性或方法的時候,它會到原型物件去尋找該屬性或方法。如果例項物件自身就有某個屬性或方法,則不會再去原型物件尋找這個屬性或方法。
原型物件的作用,是定義所有例項物件共享的屬性和方法。這也是被稱為原型物件的原因。例項物件可以視作從原型物件衍生出來的子物件。
6. JavaScript規定,所有物件都有自己的原型物件(`prototype`)。任何一個物件,都可以充當其他物件的原型;而由於原型物件也是物件,所以它也有自己的原型。這就形成一個"原型鏈"(`prototype chain`):物件到原型,再到原型的原型...
7. 所有物件的原型最終都可以上溯到`Object.prototype`,即`Object`建構函式的`prototype`屬性。所有物件都繼承了`Object.prototype`的屬性。
比如所有物件都有`valueOf`和`toString`方法,就是從`Object.prototype`繼承的
而`Object.prototype`物件的原型是`null`。原型鏈的盡頭是`null`
`null`沒有任何屬性和方法,也沒有自己的原型
```js
Object.getPrototypeOf(Object.prototype) // null
```
8. 如果物件自身和它的原型,都定義了一個同名屬性,則優先讀取物件自身的屬性,這叫做"覆蓋"(`overriding`)。
9. `prototype`物件有一個`constructor`屬性,預設指向`prototype`物件所在的建構函式。
```js
function P() {}
P.prototype.constructor === P // true
```
`constructor`屬性的作用是,可以得知某個例項物件由哪一個建構函式產生。另外,有了`constructor`屬性就可以從一個例項物件新建另一個例項。
```js
function Constr() {}
var x = new Constr();
var y = new x.constructor();
y instanceof Constr // true
```
藉助`constructor`可以在例項方法中呼叫自身的建構函式
```js
Constr.prototype.createCopy = function () {
return new this.constructor();
};
```
10. `constructor`屬性表明了原型物件與建構函式之間的關聯關係。因此如果修改原型物件,一般需要同時修改`constructor`屬性
```js
function Person(name) {
this.name = name;
}
Person.prototype.constructor === Person // true
Person.prototype = {
method: function () {}
};
Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true
```
**修改原型物件時,一般要同時修改`constructor`屬性的指向**
```js
// 壞的寫法
C.prototype = {
method1: function (...) { ... },
// ...
};
// 好的寫法
C.prototype = {
constructor: C,
method1: function (...) { ... },
// ...
};
// 更好的寫法
C.prototype.method1 = function (...) { ... };
```
11. `constructor`屬性的`name`屬性返回建構函式的名稱。
12. `instanceof`表示物件是否為某個建構函式的例項。`instanceof`做判斷時會檢查右邊建構函式的原型物件(`prototype`)是否在左邊例項物件的原型鏈上。
```js
v instanceof Vehicle
// 等同於
Vehicle.prototype.isPrototypeOf(v)
```
`instanceof`會檢查整個原型鏈,因此使用`instanceof`判斷時,例項物件的原型鏈上可能返回多個建構函式的原型物件
```js
var d = new Date();
d instanceof Date // true
d instanceof Object // true
```
任意物件(除了`null`)都是`Object`的例項。
```js
var nullObj=null;
typeof nullObj === 'object' && !(nullObj instanceof Object); // true
```
如果一個物件的原型是`null`,`instanceof`的判斷就會失真。
利用`instanceof`可以解決呼叫建構函式時忘了加`new`的問題
13. 建構函式的繼承
**子類整體繼承父類**
一、在子類的建構函式中呼叫父類的建構函式
```js
function Sub(value) {
Super.call(this); // 繼承父類例項的屬性
this.prop = value;
}
// 或者使用另一種寫法
function Sub() {
this.base = Super;
this.base();
}
```
二、讓子類的原型指向父類的原型,繼承父類原型
```js
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
```
使用`Object.create(Super.prototype)`賦值給子類的原型,防止引用賦值,後面的修改影響父類的原型。
上面是比較正確或嚴謹的寫法。比較粗略的寫法是直接將一個父類例項賦值給子類的原型
```js
Sub.prototype = new Super();
```
這種方式在子類中會繼承父類例項的方法(通常可能不需要具有父類的例項方法),不推薦
**子類中繼承父類的單個方法**
```js
ClassB.prototype.print = function() {
ClassA.prototype.print.call(this);
// self code
}
```
14. **多重繼承**:JavaScript不提供多重繼承功能,即不允許一個物件同時繼承多個物件。
但是可以通過合併兩個父類的原型的形式,間接變通的實現多重繼承
```js
function M1() {
this.hello = 'hello';
}
function M2() {
this.world = 'world';
}
function S() {
M1.call(this);
M2.call(this);
}
// 繼承 M1
S.prototype = Object.create(M1.prototype);
// 繼承鏈上加入 M2
Object.assign(S.prototype, M2.prototype);
// 指定建構函式
S.prototype.constructor = S;
var s = new S();
s.hello // 'hello'
s.world // 'world'
```
這種子類`S`同時繼承了父類`M1`和`M2`的模式又稱為 `Mixin`(`混入`)
15. `JavaScript`不是一種模組化程式語言,`ES6`才開始支援"類"和"模組"。但是可以利用物件實現模組的效果
16. 模組是實現特定功能的一組屬性和方法的封裝。所以模組的實現最簡單的方式就是把模組寫成一個物件,所有模組成員都位於物件裡面
- **把模組寫成一個物件**
```js
var module1 = new Object({
_count : 0,
m1 : function (){
//...
},
m2 : function (){
//...
}
});
```
函式`m1`、`m2`和屬性`_count`都封裝在`module1`物件中。使用中直接呼叫這個物件的屬性即可。
但是,這種寫法暴露了所有的模組成員,內部狀態可以被外部改寫。比如,在外部直接改寫內部`_count`的值:`module1._count = 5;`
- **使用建構函式封裝私有變數**
如下,通過建構函式封裝例項的私有變數
```js
function StringBuilder() {
var buffer = [];
this.add = function (str) {
buffer.push(str);
};
this.toString = function () {
return buffer.join('');
};
}
```
如下,私有變數`buffer`在例項物件中,外部是無法直接訪問的。
但是,這種方法將私有變數封裝在建構函式中,建構函式會和例項物件一直存在於記憶體中,無法在使用完成後清除。即建構函式的作用既用來生成例項物件,又用來儲存例項物件的資料,違背了**建構函式與例項物件在資料上相分離的原則(即例項物件的資料,不應該儲存在例項物件以外)**。同時佔用記憶體。
- **建構函式中將私有變數設定為例項屬性**
```js
function StringBuilder() {
this._buffer = [];
}
StringBuilder.prototype = {
constructor: StringBuilder,
add: function (str) {
this._buffer.push(str);
},
toString: function () {
return this._buffer.join('');
}
};
```
這樣私有變數就放在了例項物件中。但是私有變數仍然可以從外部讀寫
- **通過立即執行函式封裝私有變數**
通過"立即執行函式"(`Immediately-Invoked Function Expression`,`IIFE`),通過返回"閉包"的方法和屬性,實現將屬性和方法封裝在一個函式作用域裡面,函式內的屬性作為私有成員不被暴露。
這就是js模組的基本寫法:
```js
var module1 = (function () {
var _count = 0;
var m1 = function () {
//...
};
var m2 = function () {
//...
};
return {
m1 : m1,
m2 : m2
};
})();
```
- **模組的放大模式**
如果一個模組很大,必須分成幾個部分,或者一個模組需要繼承另一個模組,這時可以採用"放大模式"(`augmentation`)。
如下,為模組`module1`新增新方法,並返回新的`module1`模組
```js
var module1 = (function (mod){
mod.m3 = function () {
//...
};
return mod;
})(module1);
```
- **"寬放大模式"(`Loose augmentation`)**
在立即執行函式的引數中新增空物件,防止載入一個不存在的物件,從而報錯或出意外
```js
var module1 = (function (mod) {
//...
return mod;
})(window.module1 || {});
```
- **全域性變數的輸入**
模組最重要的是"獨立性"。因此為了在模組內部呼叫(使用)全域性變數,必須**顯式地將其他變數輸入模組內。**
比如,下面`module1`用到了jQuery庫(模組),則可以將其作為引數輸入`module1`。保證模組的獨立性,並且表明模組之間的依賴關係
```js
var module1 = (function ($) {
//...
})(jQuery);
```
立即執行函式還可以起到類似名稱空間的作用
## `Object`物件的方法
1. `Object.getPrototypeOf`方法返回引數物件的原型。這是獲取原型物件的標準方法。
幾種特殊的原型:
```js
// 空物件的原型是 Object.prototype
Object.getPrototypeOf({}) === Object.prototype // true
// Object.prototype 的原型是 null
Object.getPrototypeOf(Object.prototype) === null // true
// 函式的原型是 Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype // true
```
2. `Object.setPrototypeOf`方法為引數物件設定原型,返回該引數物件。`Object.setPrototypeOf(obj,prototypeObj)`
`new`命令可以使用`Object.setPrototypeOf`方法模擬。
```js
var F = function () {
this.foo = 'bar';
};
var f = new F();
// 等同於
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);
```
3. `Object.create`方法以一個物件為原型,返回一個例項物件。該例項完全繼承原型物件的屬性。
```js
// 原型物件
var A = {
print: function () {
console.log('hello');
}
};
// 例項物件
var B = Object.create(A);
Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true
```
`Object.create`方法的實現可以用下面的程式碼代替
```js
if (typeof Object.create !== 'function') {
Object.create = function (obj) {
function F() {}
F.prototype = obj;
return new F();
};
}
```
生成新的空物件,如下四種是等價的
```js
var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();
var obj4 = {};
```
`Object.create`的引數為`null`可以生成一個不繼承任何屬性(沒有`toString`和`valueOf`方法)的物件
```js
var obj = Object.create(null);
```
`Object.create`方法必須指定引數且為物件,否則報錯。`Object.create`建立的物件的原型是引用賦值,即動態繼承原型。
`Object.create`方法還可以接受的第二個引數是屬性描述物件,描述的物件屬性會新增到例項物件的自身屬性上。
```js
var obj = Object.create({}, {
p1: {
value: 123,
enumerable: true,
configurable: true,
writable: true,
},
p2: {
value: 'abc',
enumerable: true,
configurable: true,
writable: true,
}
});
// 等同於
var obj = Object.create({});
obj.p1 = 123;
obj.p2 = 'abc';
```
`Object.create`方法生成的物件會繼承它的原型物件的建構函式。
4. `Object.prototype.isPrototypeOf()`:例項物件的`isPrototypeOf`方法判斷該物件是否為引數物件原型鏈上的原型。
`Object.prototype`位於除了直接繼承自null的物件之外的所有物件的原型鏈上。
```js
Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // false
```
5. 關於`__proto__`屬性。`__proto__`屬性是例項物件的屬性,表示例項物件的原型(可讀寫)。例項物件(或非函式物件)無法通過`prototype`屬性獲取原型(只有引數才有`prototype`屬性),而`__proto__`屬性預設應該是私有屬性,不應該被讀寫,並且`__proto__`屬性只有瀏覽器才需要部署。因此,對原型的讀寫操作正確做法是使用`Object.getPrototypeOf()`和`Object.setPrototypeOf()`
Obj可以用`__proto__`直接設定原型
6. 關於`__proto__`和`prototype`屬性
如下,為建構函式、例項物件、普通物件中__proto__和prototype的對比
```js
/** 建構函式的__proto__和prototype **/
var P=function(){}
P.prototype
// {constructor: ƒ}
P.__proto__
// ƒ () { [native code] }
P.__proto__===P.prototype
// false
P.__proto__===P.constructor.prototype
// true
P.__proto__===Object.getPrototypeOf(P)
// true
P.__proto__===Function.prototype
// true
P.constructor===Function
// true
/** 例項物件的__proto__和prototype **/
var p=new P()
p.prototype
// undefined
p.__proto__
// {constructor: ƒ}
p.__proto__===Object.getPrototypeOf(p)
// true
p.__proto__===P
// false
p.__proto__===P.prototype
// true
p.constructor===P
// true
/** 例項物件的__proto__和prototype **/
var obj={}
obj.prototype
// undefined
obj.__proto__===Object.getPrototypeOf(obj)
// true
obj.__proto__===Object.prototype
// true
obj.constructor===Object
// true
var nullObj=Object.create(null)
nullObj.__proto__
// undefined
nullObj
// {}無屬性
```
幾點總結:
- js中,物件的原型通過`__proto__`屬性獲取,由此組成原型鏈及原型鏈的繼承。
- `__proto__`是物件自帶的屬性,除了`null`和原型物件為`null`的物件之外,所有的物件都有`__proto__`屬性。函式是物件,因此函式也有`__proto__`屬性
- `prototype`屬性是函式獨有的屬性,每個函式都有一個`prototype`屬性物件,作用是在例項物件間共享屬性和方法。因此`prototype`只會在建構函式中使用,表示例項物件的原型物件。面向物件中的繼承由此實現。
- `__proto__`屬性指向當前物件的原型物件,即建構函式的`prototype`屬性。
- `constructor`屬性表示當前物件的建構函式
- 函式也是物件,因此也擁有`__proto__`屬性,指向當前函式的建構函式的`prototype`屬性。一個函式的`constructor`是`Function`,`__proto__`是`Function.prototype`
7. `__proto__`屬性指向當前物件的原型物件,即建構函式的`prototype`屬性。
```JS
var obj = new Object();
obj.__proto__ === Object.prototype
// true
obj.__proto__ === obj.constructor.prototype
// true
```
8. 獲取一個物件`obj`的原型物件,有三種辦法:
- `obj.__proto__`
- `obj.constructor.prototype`
- `Object.getPrototypeOf(obj)`
但是 **`__proto__`屬性只有瀏覽器環境才需要部署。`obj.constructor.prototype`在手動改變原型物件時,可能會失效**
如下,將建構函式`C`的原型物件改為`p`後。例項物件`c.constructor.prototype`卻沒有指向`p`。`Object.getPrototypeOf(obj)`正確獲取原型物件,是獲取原型物件推薦使用的方法
```js
var P = function () {};
var p = new P();
var C = function () {};
C.prototype = p;
var c = new C();
c.constructor.prototype === p // false
c.constructor.prototype === P.prototype // true
Object.getPrototypeOf(c) === p // true
```
上面變更原型物件的方法是不正確的。通常**修改`prototype`時,要同時設定`constructor`屬性。**
```js
C.prototype = p;
C.prototype.constructor = C;
var c = new C();
c.constructor.prototype === p // true
```
9. `Object.getOwnPropertyNames()`返回物件自身所有屬性的鍵名組成的陣列(包括可遍歷和不可遍歷的所有屬性)。
10. `Object.keys`返回物件自身所有可遍歷的屬性名組成的陣列
11. `Object.prototype.hasOwnProperty()`返回一個屬性是否為物件自身的屬性
*hasOwnProperty方法是 JavaScript 之中唯一一個處理物件屬性時,不會遍歷原型鏈的方法*
12. `in`運算子表示一個物件是否具有某個屬性。即檢查一個屬性是否存在。
```js
'length' in Date // true
'toString' in Date // true
```
`for...in`迴圈可以獲取一個物件所有可遍歷的屬性(自身和繼承的屬性)
通常使用如下方式,遍歷物件自身的屬性
```js
for ( var name in object ) {
if ( object.hasOwnProperty(name) ) {
/* loop code */
}
}
```
13. 獲取一個物件的所有屬性(包含自身的和繼承的,以及可列舉和不可列舉的所有屬性)
```js
function inheritedPropertyNames(obj) {
var props = {};
while(obj) {
Object.getOwnPropertyNames(obj).forEach(function(p) {
props[p] = true;
});
obj = Object.getPrototypeOf(obj);
}
return Object.getOwnPropertyNames(props);
}
```
14. 物件的拷貝
要拷貝一個物件,需要做到下面兩點:
- 確保拷貝後的物件,與原物件具有同樣的原型。
- 確保拷貝後的物件,與原物件具有同樣的例項屬性。
如下,為物件拷貝的實現:
```js
function copyObject(orig) {
var copy = Object.create(Object.getPrototypeOf(orig));
copyOwnPropertiesFrom(copy, orig);
return copy;
}
function copyOwnPropertiesFrom(target, source) {
Object
.getOwnPropertyNames(source)
.forEach(function (propKey) {
var desc = Object.getOwnPropertyDescriptor(source, propKey);
Object.defineProperty(target, propKey, desc);
});
return target;
}
```
利用`ES2017`引入的`Object.getOwnPropertyDescriptors`可以更簡便的實現
```js
function copyObject(orig) {
return Object.create(
Object.getPrototypeOf(orig),
Object.getOwnPropertyDescriptors(orig)
);
}
```
## 嚴格模式(`strict mode`)
1. JavaScript提供程式碼執行的第二種模式:嚴格模式。嚴格模式從ES5引入,主要目的為:
- 明確禁止一些不合理、不嚴謹的語法,減少 JavaScript 語言的一些怪異行為。
- 增加更多報錯的場合,消除程式碼執行的一些不安全之處,保證程式碼執行的安全。
- 提高編譯器效率,增加執行速度。
- 為未來新版本的 JavaScript 語法做好鋪墊。
2. 嚴格模式的啟用:在程式碼頭部新增一行`'use strict';`即可。老版本的引擎會把它當作一行普通字串,加以忽略。新版本的引擎就會進入嚴格模式。
3. `use strict`放在指令碼檔案的第一行,整個指令碼都將以嚴格模式執行。不在第一行則無效。
4. `use strict`放在函式體的第一行,則整個函式以嚴格模式執行。
5. 有時需要把不同指令碼檔案合併到一個檔案。這時,如果一個是嚴格模式另一個不是,則合併後結果將會是不正確的。解決辦法是可以把整個指令碼檔案放在一個立即執行的匿名函式中:
```js
(function () {
'use strict';
// some code here
})();
```
6. 嚴格模式下的顯式報錯
嚴格模式下js的語法更加嚴格,許多在正常模式下不會報錯的錯誤程式碼都會顯式的報錯
如下幾項操作嚴格模式下都會報錯:
- 只讀屬性不可寫;比如字串的`length`屬性
- 不可配置屬性無法刪除(`non-configurable`)
- 只設置了取值器的屬性不可寫
- 禁止擴充套件的物件不可擴充套件
- `eval`、`arguments` 不可用作標識名
正常模式下,如果函式有多個重名的引數,可以用`arguments[i]`讀取。嚴格模式下屬於語法錯誤。
- 函式不能有重名的引數
- 禁止八進位制的字首`0`表示。*八進位制使用數字0和字母O表示*
7. 嚴格模式下的安全限制
- 全域性變數顯式宣告
- 禁止`this`關鍵字指向全域性物件。避免無意中創造全域性變數
```js
// 正常模式
function f() {
console.log(this === window);
}
f() // true
// 嚴格模式
function f() {
'use strict';
console.log(this === undefined);
}
f() // true
```
嚴格模式下,函式直接呼叫時,內部的`this`表示`undefined`(未定義),因此可以用`call`、`apply`和`bind`方法,將任意值繫結在`this`上面。正常模式下,`this`指向全域性物件,如果繫結的值是非物件,將被自動轉為物件再繫結上去,而`null`和`undefined`這兩個無法轉成物件的值,將被忽略。
- 函式內部禁止使用 `fn.callee`、`fn.caller`
- 禁止使用`arguments.callee`、`arguments.caller`
`arguments.callee`和`arguments.caller`是兩個歷史遺留的變數,從來沒有標準化過,現在已經取消
- 禁止刪除變數。嚴格模式下使用`delete`命令刪除一個變數,會報錯。只有物件的屬性,且屬性的描述物件的`configurable`屬性設定為`true`,才能被`delete`命令刪除。
8. 靜態繫結
- 禁止使用`with`語句
- 創設`eval`作用域
正常模式下,`JavaScript`語言有兩種變數作用域(`scope`):全域性作用域和函式作用域。嚴格模式創設了第三種作用域:`eval`作用域。
`eval`所生成的變數只能用於`eval`內部。
```js
(function () {
'use strict';
var x = 2;
console.log(eval('var x = 5; x')) // 5
console.log(x) // 2
})()
```
`eval`語句使用嚴格模式:
```js
// 方式一
function f1(str){
'use strict';
return eval(str);
}
f1('undeclared_variable = 1'); // 報錯
// 方式二
function f2(str){
return eval(str);
}
f2('"use strict";undeclared_variable = 1') // 報錯
```
- `arguments`不再追蹤引數的變化。嚴格模式下引數修改,`arguments`不再聯動跟著改變
9. 面向`ECMAScript 6`
- ES5的嚴格模式只允許在全域性作用域或函式作用域宣告函式。
- 保留字。嚴格模式新增了一些保留字:`implements`、`interface`、`let`、`package`、`private`、`protected`、`public`、`static`、`yi