參考文章 You Don't Know JS: this & Object Prototypes
關於this
什麼是this
this
是在執行時綁定,而不像詞法作用域,在定義時就已經決定的。
如何判斷this綁定的位置
有四個方法可以判斷this
- 使用
new
建構子時,會強制將this
指向被new
的函式
- 使用
bind
, call
, apply
可以將this
強制轉為輸入的參數
- 呼叫時有
context object
的則以context object
的環境為this
- 沒有上述條件的,默認的
this
通常為global
, 若為嚴格模式,則this
為undefined
使用 new
建構子時,會強制將this
指向被new
的函式
function fn() {
console.log(this);
}
fn(); // this指向window
new fn(); // this指向fn本身
const obj = { a: 123 };
const bindObj = fn.bind(obj);
new bindObj() // 覆蓋原先的this(obj),還是指向fn本身
new fn().bind(obj) // TypeError: (intermediate value).bind is not a function ... 無法在new時binding
// es6 class也是一樣的
class fn {
constrcutor() {
console.log(this)
}
}
fn(); // TypeError: Class constructor test cannot be invoked without 'new' ...
new fn(); // this指向fn本身
使用bind
, call
, apply
可以將this
強制轉為輸入的參數
function fn() {
console.log(this.a);
}
var a = 2;
fn(); // 2 <- 指向window, 在嚴格模式會回傳typeError
const obj = { a: 123 };
fn.call(obj); // 123
fn.apply(obj); // 123
const bindObj = fn.bind(obj);
bindObj(); // 123
呼叫時有context object
的則以context object
的環境為this
const obj = {
a: 123,
fn: function() {
console.log(this.a);
}
}
obj.fn(); // 123, 指向obj
const obj2 = obj.fn;
obj2(); // undefined, 指向window, 因為沒`context object`, 嚴格模式則會回傳typeError
obj.fn = function() {
setTimeout(function() {
console.log(this.a); // 這個function沒有context object, 會指向全域
});
}
obj.fn(); // undefined, 指向window, 因為沒`context object`, 嚴格模式則會回傳typeError
沒有上述條件的,默認的this
通常為global
, 若為嚴格模式,則this
為undefined
function fn() {
console.log(this.a);
}
fn(); // undefined, 指向window, 嚴格模式則會回傳typeError
關於屬性描述符 (Property Descriptors)
獲得Object的屬性Object.getOwnPropertyDescriptor
const obj = {
a: 2
};
Object.getOwnPropertyDescriptor(obj, 'a');
/**
* {
* value: 2
* writable: true
* enumerable: true
* configurable: true
* }
**/
定義Object的屬性Object.defineProperty
const obj = {};
Object.defineProperty(obj, 'a', {
value: 2,
writable: true,
configurable: true,
enumerable: true
});
console.log(obj); // {a : 2}
可寫性 (Writable)
可以控制Object的property能不能被改寫
const obj = {};
Object.defineProperty(obj, 'a', {
writable: false // 不能改寫
});
obj.a = 3; // 結果obj.a還是2,且在嚴格模式下會是TypeError: Cannot assign to read only property 'a' of object
可配置性 (Configurable)
決定Object是否可以調用defineProperty
來調整定義,且configurable
設為false
, 就不能再設回true
,屬於單向操作。
const obj = {};
Object.defineProperty(obj, 'a', {
configurable: false // 不可配置
});
Object.defineProperty(obj, 'a', {
configurable: true
}); // TypeError: Cannot redefine property
// 但是若是將writable從true改為false卻是可行的
Object.defineProperty(obj, 'a', {
writable: false // true -> false 可行
});
// 不過將writable從false變true則會失敗
Object.defineProperty(obj, 'a', {
writable: true // false -> true 不可行
}); // TypeError: Cannot redefine property
可枚聚性 (Enumerable)
決定Object在for..in
時,會不會顯示,也就是可不可以在迭代中出現。
const obj = {
a: 2,
b: 3,
c: 4,
d: 5
};
Object.defineProperty(obj, 'a', {
enumerable: false // 設定a不會被遞迴
});
for(let i in obj) {
console.log(i);
}
// b
// c
// d
obj.propertyIsEnumerable('a'); // false
防止擴展 (PreventExtensions)
使用Object.preventExtensions
可以防止Object被添加新的屬性
const obj = {
a: 123
};
Object.preventExtensions(obj);
obj.b = 2; // undefined, 若在嚴格模式則回傳TypeError: Cannot add property b, object is not extensible
封印 (Seal)
使用Object.seal
等同於同時使用Object.preventExtensions
跟將configurable
設為false
const obj = {
a: 123
};
Object.seal(obj);
obj.b = 2 // undefined, 若在嚴格模式則回傳TypeError: Cannot add property b, object is not extensible
Object.defineProperty(obj, 'a', {
configurable: true
}); // TypeError: Cannot redefine property
凍結 (Freeze)
使用Object.Freeze
等同於同時使用Object.seal
跟將writable
設為false
const obj = {
a: 123
};
Object.freeze(obj);
obj.b = 2 // undefined, 若在嚴格模式則回傳TypeError: Cannot add property b, object is not extensible
obj.a = 234; // 結果obj.a還是234,且在嚴格模式下會是TypeError: Cannot assign to read only property 'a' of object
存在性 (Existence)
使用Object.prototype.hasOwnProperty
跟in
都可以檢查屬性是否在Object中,但in
會額外查詢prototype
const obj = {
a: 123
};
console.log('a' in obj); // true
console.log('toString' in obj); // true, 因為toString在prototype中
// 不使用obj.hasOwnProperty是為了避免obj的prototype有被竄改的問題
console.log(Object.prototype.hasOwnProperty.call(obj, 'a')); // true
console.log(Object.prototype.hasOwnProperty(obj, 'toString')); // false, 因為只檢查property
// 這時大家應該會想測試看看enumerable是不是會影響,答案是不會影響
obj.b = 345;
Object.defineProperty(obj, 'b', {
enumerable: false
});
for (let i in obj) { console.log(i); } // a
console.log(Object.toString.propertyIsEnumerable()); // false, 所以for..in不會顯示
console.log('b' in obj); // true
console.log(Object.prototype.hasOwnProperty.call(obj, 'b')); // true
Object的其他應用
這個小節紀錄一些Object可以使用的方法
const obj = {
a: 1,
b: 2,
c: 3
};
Object.keys(obj); // ['a', 'b', 'c']
Object.values(obj); // [1, 2, 3]
Object.entries(obj); // [['a', 1], ['b', 2], ['c', 3]]
for(let i in obj) {
console.log(i);
}
// a
// b
// c
for(let i of obj) {
console.log(i); // TypeError: obj is not iterable
}
/* 試著實作Symbol.iterator到Object */
obj[Symbol.iterator] = function() {
const values = Object.values(this);
let index = 0;
return {next: function() {
return {
value: values[index],
done: index++ === values.length
};
}
}
}
const it = obj[Symbol.iterator]();
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }
it.next(); // { value: undefined, done: true }
for(let i of obj) {
console.log(i);
}
// 1
// 2
// 3 <- 跑到done為true就不會再繼續顯示了
關於prototype
每個Object都內建prototype
,並且像是chain一樣,一層一層的連結
難以捉模的prototype
一個Object
在呼叫函式或變數時,若是找不到時,會往父層的prototype
尋找,直到盡頭Object.prototype
為止
// 定義了obj有a屬性
const obj = {
a: 123
};
// 呼叫了obj沒有定義的toString
obj.toString(); // [object Object], 可以成功的原因是因為這個方法來自於Object.prototype.toString
釐清prototype
之間的關係
A instanceof B
代表B.prototype
是否在A
的prototype chain
中
__proto__
代表該Object
指向的prototype位置
function Fn() {};
// __proto__就是建構者的prototype
Fn.__proto__ === Function.prototype // true
// fn創建時會將__proto__指向Function.prototype
Fn instanceof Function; // true
// 而Function本身的__proto__是指向Object.prototype
Fn instanceof Object; // true
// fn本身沒constructor, 但指向的prototype有,所以來自Function.prototype,且Fn是由Function建構
Fn.constructor === Function; // true
// Function.prototype在Fn的prototype chain上嗎
Function.prototype.isPrototypeOf(Fn); // true
// 拿取創建Fn的prototype, 也就是Function的prototype做比較
Object.getPrototypeOf(Fn) === Function.prototype; // true
// Fn有自己的prototype, Function也有自己的prototype
// 且Fn的__proto__指向Function.prototype
// 所以Fn.prototype不會影響到Fn的取值,只有Function.prototype會
// ex: Fn.prototype.test = 1, Function.prototype.test = 2
// Fn.test會是回傳2
Fn.prototype === Function.prototype; // false
// 從以上推下來後,來測驗一下
const fn = new Fn();
fn instanceof Fn; // true
fn.constrctor = Fn; // true
fn.__proto__ === Fn.prototype; // true
prototype
不為人知的設定
const obj = {};
obj.a = 1;
這個賦值的行為,會觸發一些事情,當a
原本不存在在obj
裡,但存在在obj
指到的prototype
中時,會觸發下列判斷
- 訪問到
prototype
中的a
,且a
的writable
為true
,則會再創建一個a
到obj
中。
- 訪問到
prototype
中的a
,且a
的writable
為false
,則不會有任何覆寫,且在嚴格模式時,會拋出錯誤。
- 訪問到
prototype
中的a
,且a
是個setter
,則不會像第一個一樣,被重複創建,而只是被呼叫而已。
訪問到prototype
中的a
,且a
的writable
為true
,則會再創建一個a
到obj
中
const obj1 = { a: 1 }; // { a: 1 }
const obj2 = Object.create(obj1); // {} , prototype指向obj1
console.log(obj2.a); // 1
obj1.a = 2;
console.log(obj2.a); // 2
obj2.a = 3;
console.log(obj1.a); // 2
console.log(obj2.a); // 3
訪問到prototype
中的a
,且a
的writable
為false
,則不會有任何覆寫,且在嚴格模式時,會拋出錯誤
const obj1 = Object.defineProperty({}, 'a', {
value: 1,
writable: false
});
const obj2 = Object.create(obj1);
obj2.a = 2; // 不會有作用,且在嚴格模式時會拋出TypeError: Cannot assign to read only property 'a' of object
訪問到prototype
中的a
,且a
是個setter
,則不會像第一個一樣,被重複創建,而只是被呼叫而已
const obj1 = Object.defineProperty({b: 10}, 'a', {
get: function() { return this.b * 2; },
set: function(val) { this.b = val; }
}); // { b: 10};
const obj2 = Object.create(obj1); // {}
console.log(obj1.a); // 20
console.log(obj2.a); // 20
obj1.a = 2;
console.log(obj1.a); // 4
console.log(obj2.a); // 4
obj2.a = 4;
console.log(obj1.a); // 4
console.log(obj2); // { b: 4 }
console.log(obj2.a); // 8, 會不一樣是因為b在obj2.a的setter中創建在obj2中了