你不知道的JavaScript·第二部分

曾田生z發表於2018-07-09

接著上一篇 你不知道的JavaScript·第一部分

第一章: 關於this

this 到底是什麼

this 是在 執行時 繫結的,this的繫結和函式宣告的位置沒有任何關係,只卻取決於函式的 呼叫方式

function foo() {
  this.bar(); // this指向window,相當於 window.bar()
}
function bar() {
  console.log('---bar---');
}
foo()
複製程式碼

上段程式碼在瀏覽器下是能正常執行的,函式 bar 能被正常呼叫,因為函式 bar 是宣告在全域性的,全域性的 this 指向 window , 執行函式 foo 時也是在全域性(window)下執行的,所以函式內部的 this 也是指向 window 的,相當於在函式 foo 內部 是 window.bar() ,自然能正常呼叫執行。

但在嚴格模式下 'use strict' , 全域性 this 不指向 window,而是 undefined,所以在嚴格模式下上面程式碼會報錯

第二章: this全面解析

1、呼叫位置

一個很簡單的判斷 this 指向誰就看函式在程式碼的呼叫位置

例子1

下面這個例子第一章節分析的一樣,函式 foo 在全域性呼叫, 在瀏覽器下 this 指向 wwindow,當然要是考慮嚴格模式, this.a 會報錯 a is not defined

a = 1;
function foo() {
  console.log(this.a); // 1
}
foo();
複製程式碼

例子2

下面例子雖然同一個函式 foo ,但卻被不同的物件呼叫,this 分別指向它的呼叫者

function foo() {
  console.log(this.a);
}
var obj1 = {
  foo: foo,
  a: 1
}

var obj2 = {
  foo: foo,
  a: 2
}
obj1.foo() // 1
obj2.foo() // 2
複製程式碼

特殊例子3

下面例子雖然函式 foo 宣告在全域性,但被利用 callapplybind 顯式的繫結了物件,this 指向顯式繫結的物件

function foo() {
  console.log(this.a);
}
var obj1 = {
  a: 1
}
var obj2 = {
  a: 2
}
var obj3 = {
  a: 3
}
foo.call(obj1) // 1
foo.apply(obj2) // 2
foo.bind(obj3)() // 3
複製程式碼

例子4

下面例子使用了 new 繫結,this 指向了該物件

function foo(a) {
  this.a = a
}
var bar = new foo(1)
console.log(bar.a);
複製程式碼

使用 new 來呼叫函式經歷了以下步驟:

1、建立或者說構造一個全新的物件

2、這個新物件會被執行[Prototype]連線

3、這個新物件會被繫結到函式呼叫的 this

4、如果這個函式沒有返回其他物件,那麼 new 表示式中的函式呼叫會自動返回這個新物件

小結:

經過上面的例子分析,判斷 this 的指向可以羅列幾個點,

1、看呼叫者,函式被誰呼叫 this 指向誰

2、是否有 callapplybind 顯式的改變了 this 的指向

3、函式是否是被 new 建立的,是的話 this 指向 new 出來的物件

為什麼 this 的指向不固定呢,因為 JavaScript 是‘解釋執行’ 語言,邊編譯邊執行的,this 的指向也就動態的。

第三章: 物件

物件再熟悉不過了,物件的基本要素就是屬性和方法,但有時候程式碼寫著寫著就忘了物件本來的特徵。

1、內建物件

String

var str = 'i am a string';
str.length ;      // 13
str.charAt(3);    // m
複製程式碼

字串經常使用,沒但發覺字串也會有屬性和方法,這個特性應該物件才有的呀。原來JavaScript 引擎自動把字串轉換成 String 物件,所以可以訪問屬性和方法。

Array

陣列也是 JavaScript 的內建物件,那既然是物件是不是也可以像物件那樣賦值和操作呢。

var arr = ['foo', 'bar'];
arr.baz = 'baz';
console.log(arr.baz);
複製程式碼

上面例子不像以往那樣運算元組,而是按物件的形式進行操作,是可行的。但這裡也是為了給大家解釋陣列也是物件這麼個基本概念,一般不將陣列當做普通鍵值物件來使用。

內建物件除了上面提到的 String,Array,還有 Number、Boolean、Object、Function、Date、RegExp、Error

2、屬性描述符

var obj = {
  a: 1
}
複製程式碼

物件 obj 的屬性 a 就單單記錄一個數值 1 嗎,其實不是的,我們們用 getOwnPropertyDescriptor 列印下屬性 a 看下輸出資訊

console.log(Object.getOwnPropertyDescriptor(obj, 'a'));
// 輸出
{
  value: 1, 
  writable: true,
  configurable: true,
  enumerable: true
}
複製程式碼

發現列印出來好幾個屬性,這幾個屬性來解釋一下

writable

writable 決定是否可以修改屬性值

var obj = {}
Object.defineProperty(obj, 'a', {
  value: 1,
  writable: false,
  enumerable: true,
  configurable: true
})
obj.a = 2
console.log(obj.a); // 1
複製程式碼

writable 屬性置為 false 就無法更改屬性的值了

configurable

configurable 屬性用來描述物件的屬性是否可配置,也就是 configurable = false,接下去程式碼想要將 configurable = true 將會報錯,因為物件的屬性已經不可配置了

enumerable

enumerable 屬性用來設定該物件屬性是否可列舉, enumerable = false 那麼該物件屬性將不在列舉中,也就是 for...in 將不會遍歷到該屬性

3、Getter和Setter

物件還有兩個隱藏的函式會被忽略,因為是 JavaScript 的預設操作,沒特殊用法的話就不會去修改它,如果重新修改了 Getter和Setter 隱藏函式,JavaScript會忽略它們的 value 和 writable 特性

下面一個例子對屬性 a 進行 Getter和Setter 重寫

var obj = {
  get a() {
    return this._a_
  },
  set a(val) {
    this._a_ = val * 2
  }
}

obj.a = 2
console.log(obj.a); // 4
複製程式碼

因為對賦值和取值時都會觸發相應的 get 和 set 方法,那就可以利用這個特性做一些比如訊息的釋出訂閱,事件通知等高階用法了

第五章: 原型

JavaScript 物件有一個特殊的 [Prototype] 屬性,幾乎所有物件在建立時 [Prototype] 屬性都會被賦予一個非空的值。

1、Prototype

var anotherObj = { a: 1}
var obj = Object.create(anotherObj)
console.log(obj.a);  // 1
複製程式碼

Object.create() 會把一個物件的 [Prototype] 關聯到另一個物件上,上面例子物件 obj 的 [Prototype] 被關聯到了物件 anotherObj 上,當訪問物件 obj 的屬性 a 時,如果物件上不存在該屬性那麼 [Prototype] 鏈就會被遍歷,[Prototype] 鏈上找到對應的屬性就會返回。到哪裡才是 [Prototype] 鏈的‘盡頭’呢,[Prototype] 鏈最終會指向JavaScript 內建的 Object.prototype

2、"類"函式

function Foo() {
  // ...
}
var a = new Foo()
複製程式碼

上面例子的 Foo 是一個"類"函式,或被叫做 "建構函式" ,為什麼叫做"類"函式而不直接叫做"類"呢,其實 JavaScript 中只有物件,它並沒有類,es6 的類也只是一個語法糖而已。

被習慣稱作"類"的原因是因為 JavaScript 有個 new 操作符,所以習慣的和其他語言一樣稱 new 後面的函式為一個類。new Foo() 只是間接完成了一個目的:一個關聯到其他物件的新物件。也就是 new 操作符的作用就是建立一個關聯物件而已。

Foo 其實也只是一個函式, 沒有 new 操作符, Foo() 也能正常執行,只是當且僅當使用 new 時,函式呼叫會變成 ‘建構函式呼叫’。

3、原型鏈

function Foo() {
  // ...
}
Foo.prototype // {}
複製程式碼

所有的函式預設都會有一個名為 prototype 的共有並且不可列舉的屬性,prototype即為函式的原型

function Foo() {
  // ...
}
var a = new Foo()
a.__proto__ // {}
複製程式碼

所有的物件預設都會有一個名為 __proto__ 的共有並且不可列舉的屬性

prototype__proto__ 有什麼聯絡呢?

a.__proto__ === Foo.prototype
複製程式碼

每個物件的 __proto__ 屬性指向函式的原型。

因為每個物件都有 __proto__ 屬性指向函式的 prototype 所以往函式的 prototype 新增屬性或方法,每個物件都能訪問的到

function Foo() {
  // ...
}
Foo.prototype.age = 233
var a = new Foo()
var b = new Foo()
a.age // 233
b.age // 233
複製程式碼

4、物件關聯

如果在第一個物件上沒有找到需要的屬性或者方法引用,JavaScript引擎會繼續在 [Prototype] 關聯的物件上進行查詢。同理,如果在後者中也沒有找到需要的引用就會繼續查詢它的 [Prototype] ,以此類推。這一系列物件的連結被稱為 ‘原型鏈’ 。

‘原型鏈’ 在上面已經提到和解釋了,這裡更加深入的去理解什麼是 ‘原型鏈’ 。

‘原型鏈’ 這種機制的本質其實是物件之間的關聯關係:物件關聯


var Obj1 = {
  name: '曾田生',
  setID: function(ID) {
    console.log(ID)
  }
}

var Obj2 = Object.create(Obj1)

Obj2.age = 233

var Obj3 = Object.create(Obj2)

console.log(Obj3.age)  // 233
console.log(Obj3.name) // 曾田生
Obj3.setID('ABCD')     // ABCD
console.log(Obj3)
複製程式碼

我們使用 Object.create 將一個個物件 關聯 起來,當訪問到自身物件沒有的方法或屬性時,就會去它關聯的物件查詢,這其實就是 [Prototype] 的機制。

花了兩個篇幅來整理 《你不知道的JavaScript·上卷》,希望各位看官有所收穫,感興趣的同學可以前往我的 github 檢視其它博文 ,歡迎 star

相關文章