林大媽的JavaScript進階知識(一):對象與內存


JavaScript中的基本數據類型

在JS中,有6種基本數據類型:

  1. string
  2. number
  3. boolean
  4. null
  5. undefined
  6. Symbol(ES6)

除去這六種基本數據類型以外,其他的所有變量數據類型都是Object。基本類型的操作在JS底層中是這樣實現的:

// 1. 申請一塊內存,存儲foo變量的內容為1
let foo = 1
// 2. 定義foo為1時,foo的數據類型是number
typeof foo // "number"
// 3. 我們知道,const的意思是constant(常量,無法改變的)
const bar = foo
// 4. 修改值時,新申請了一塊內存存儲foo的內容為2
foo = 2
// 4. 則會發現,foo已經是2了,bar仍然是1
console.log(foo) // 2
console.log(bar) // 1
由此可見,我們定義的變量實際上都是指針。基本數據類型的修改實際上是新申請一塊內存地址,將這個指針指向新的內存地址。使用const定義變量,實際上相當於定義了一個指針常量,指向固定的地址不能被修改。

JavaScript中的對象

定義和修改對象

我們來試着從變量定義的執行結果看出它在底層的執行方式:

// 1. 定義一個對象
const obj = {
	foo: 1
}
// 2. 定義一個新變量與其相等
const anotherObj = obj
// 3. 修改這個對象
obj.foo = 2
// 4. 發現兩個對象都修改了
console.log(obj)
console.log(anotherObj)
由此可見,JS中對象的賦值是一種淺拷貝。

熟悉了對象的本質以后,我們要逐步了解對象有哪些特性。

對象的屬性與方法

實際上,在學習一般高級語言的時候,應該先介紹類的屬性與方法(共性),才介紹實例化類產生的對象如何使用(特性)。但由於JS是以原型、對象為主的語言,類只能在ES6中以語法糖的形式存活,我們只能先從對象入手,反推類的性質。

對象其實就是一些屬性和一些方法的集合。而對象的屬性和方法要深究,其實也是非常復雜的問題(光看內置對象Object以及Object.prototype上有多少方法處理對象的屬性就知道不簡單):

屬性

描述符

每個屬性上有描述符號。所謂的描述符號,是一些鍵值對,它們描述了對於這個屬性是否能操作、是否能枚舉等等的所有特性。

描述符號只能是數據描述符和存取描述符兩個里面的一個(在一般的聲明中,屬性默認含有的是數據描述符)。

首先,這兩種描述符公有的兩個屬性是:configurable(這個屬性的描述符是否能被修改、以及這個屬性是否能被delete運算符刪除)和enumerable(是否能被枚舉)。

然后是數據描述符,顧名思義,它定義了value(值)和writable(值是否能被賦值語句修改)。

最后是存取描述符,同樣的顧名思義,它定義了這個屬性的get(讀取時執行的函數)和set(修改時執行的函數)。

定義和修改屬性

我們可以通過Object.defineProperty來具體地配置一個屬性的描述符:

const foo = {}
// 1. 數據描述符
Object.defineProperty(foo, 'bar', {
	// 1.1. 固定了是數據描述符不能被修改
	configurable: false,
	// 1.2. 設置該屬性可以枚舉
	enumerable: true,
	// 1.3. 值為3
	value: 3,
	// 1.4. 無論怎么賦值更新foo.bar,它的值仍然是3
	writable: false
})
// 2. 存取描述符
let baz = 3
Object.defineProperty(foo, 'baz', {
	// 2.1. 固定了是存取描述符不能被修改
	configurable: false,
	// 2.2. 設置該屬性可以枚舉
	enumerable: true,
	// 2.3. 使用foo.baz讀取時,會順帶輸出這句話
	get: function () {
		console.log('The getter is called.')
		return baz
	},
	// 2.4. 使用賦值語句為foo.baz賦值時,會順帶輸出這句話
	set: function (value) {
		console.log('The setter is called.')
		baz = value
	}
})
由此可見,定義屬性時可以根據自己的需求修改默認的描述符。

了解到這里,我們不難聯想到,著名的前端框架Vue實現數據的雙向綁定,實際上就是利用了這個存取描述符。我們在編寫Vue代碼時,定義Vue對象中data屬性的值。Vue在編譯過程中,首先收集了這個值的所有依賴(也就是它在我們代碼中出現的各種地方),然后利用Object.defineProperty,把屬性的描述符改成存取描述,並在setter中修改所有的依賴,通知視圖更新。這樣就有了我們覺得非常神奇的數據雙向綁定。

遍歷屬性

對屬性的常用操作除了定義與修改,還有遍歷。最常用的遍歷方法是:

// 兩種方法,都只能遍歷enumerable的屬性
const obj = {
	foo: 1,
	bar: 2,
	baz: 3
}
// 1. Object.keys
let objAttrs = Object.keys(obj)
// 2. for...in...
let objAttrs = []
for (let key in obj) {
	objAttrs.push(key)
}
// 以上兩種遍歷的方法數量和順序均一致
// 如果不希望遍歷原型上的屬性,還可以使用Object.hasOwnProperty進行過濾
遍歷時需要考慮到屬性是否能被枚舉以及原型上的屬性是否需要被遍歷到。

方法

函數調用

函數有總共四種調用模式,這四種調用模式其實都是圍繞着this指向的不同而定的(下面的全局在瀏覽器環境中表示window,在node環境中表示global):

  1. 普通函數調用 —— this指向全局
  2. 方法調用 —— this指向方法所定義的對象
  3. 構造器調用 ——(使用new關鍵字時)this指向當前函數對象(函數本身就是對象)
  4. (call、apply和bind)調用 —— this指向(call、apply和bind)函數的第一個參數
方法是什么

方法就是定義在類或者對象上,用來處理對象有關數據的函數。簡而言之,方法就是函數的子集。方法特別於其他函數的點在於,它的this是指向當前對象的。

從方法到this

由此可見,我們通過不同的方式調用函數,最終為的還是根據自己的需求定義this的指向。我們試着來區分幾個例子,從而最終總結出JS中this的指向情況:

一般情況
// 定義一個對象,里面有一個輸出對象自身的方法
const obj = {
  foo: function () {
    return this
  }
}
// 直接執行obj.foo方法,正常得到obj對象
console.log(obj.foo())
// 用一個外部變量接收obj.foo方法
const fakeFoo = obj.foo
// 執行這個接收回來的方法,獲得this為全局對象
console.log(fakeFoo())
上述例子說明,一般情況下,this指向的是函數被調用時所在的上下文環境。
(所謂函數調用時的上下文環境,實際上也等同於JS中的詞法作用域(lexical scope),即函數作用域)
內部函數

下面再來看看內部函數的this指向:

// 定義一個對象,里面有一個方法,方法里面有一個返回this的內部函數
// 以此測試內部函數中this指向
const obj = {
	foo: function () {
		return function () {
			return this
		}
	}
}
// 執行這個內部的函數,發現this指向的是全局對象
console.log(obj.foo()())
上述例子說明,內部函數中,this沒有指向當前對象,而是指向的是全局。
箭頭函數

當然,ES6中箭頭函數的出現修復了這些問題,內部函數的this也能正確指向當前對象了:

// 僅僅把上述對象的內部函數換為箭頭函數
const obj = {
	foo: function () {
		return () => {
			return this
		}
	}
}
// 正確得到this為當前對象
console.log(obj.foo()())
上述例子說明,箭頭函數把this綁定回了詞法作用域。

但是,由於JS的詞法作用域為函數作用域,以下的寫法又會發生錯誤:

const obj = {
	foo: () => {
		return this
	}
}
// 得到的this為全局對象
console.log(obj.foo())
上述例子說明了,由於JS詞法作用域為函數作用域,箭頭函數沒有外部函數包着,因此是全局作用域。

但是,箭頭函數強制將this綁定到函數執行的上下文環境。這導致了bind、call與apply的失效。

// 定義一個對象,里面有一個方法返回當前對象的foo屬性
// 並將這個方法應用到foo為2的新對象上
const obj = {
  foo: 1,
  bar: function () {
    const foo = this.foo
    const baz = function () {
    	return foo
    }
    return baz.call({ foo: 2 })
  }
}
// 得到新對象的值為2
console.log(obj.bar())
正常情況下,call方法正常地將這個方法應用到另一個對象上。
// 僅將內部返回foo的函數改為箭頭函數
const obj = {
  foo: 1,
  bar: function () {
    const foo = this.foo
    const baz = () => {
    	return foo
    }
    return baz.call({ foo: 2 })
  }
}
// 得到的還是舊的1,說明call方法並沒有成功將this綁定到新對象上
console.log(obj.bar())
而箭頭函數的this則被緊鎖在了舊對象上。

總結:

  • JS的6種基本數據類型:string、number、boolean、null、undefined、Symbol
  • JS中,除了6種基本數據類型以外,其他變量都是對象,我們通過操作指針對這些對象進行處理
  • 對象的屬性有兩種描述符的其中一種:數據描述符(默認)和存取描述符
  • 對象的方法中,this默認指向這個對象,而方法的內部函數this默認指向全局

拓展:

  • 通過Object.defineProperty可以定義和修改某個屬性的描述符
  • 普通函數中的this默認指向詞法作用域,使用new定義對象、call、apply、bind等內建方法,可以修改this的指向
  • 箭頭函數將this鎖在了詞法作用域,沒辦法使用call、apply、bind進行修改


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM