1. 程式人生 > >《JavaScript語言入門教程》記錄整理:面向物件

《JavaScript語言入門教程》記錄整理:面向物件

[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