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