Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

You Don't Know JS 小筆記 - this & prototype #4

Open
marshal604 opened this issue Jun 3, 2020 · 0 comments
Open

You Don't Know JS 小筆記 - this & prototype #4

marshal604 opened this issue Jun 3, 2020 · 0 comments
Labels
Front-End article is about front-end

Comments

@marshal604
Copy link
Owner

參考文章 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, 若為嚴格模式,則thisundefined

使用 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, 若為嚴格模式,則thisundefined

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.hasOwnPropertyin都可以檢查屬性是否在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是否在Aprototype 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,且awritabletrue,則會創建一個aobj中。
  • 訪問到prototype中的a,且awritablefalse,則不會有任何覆寫,且在嚴格模式時,會拋出錯誤。
  • 訪問到prototype中的a,且a是個setter,則不會像第一個一樣,被重複創建,而只是被呼叫而已。

訪問到prototype中的a,且awritabletrue,則會創建一個aobj

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,且awritablefalse,則不會有任何覆寫,且在嚴格模式時,會拋出錯誤

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中了
@marshal604 marshal604 added the Front-End article is about front-end label Jun 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Front-End article is about front-end
Projects
None yet
Development

No branches or pull requests

1 participant