林大媽的JavaScript進階知識(一):對象與記憶體

来源:https://www.cnblogs.com/BlogOfMotherLyn/archive/2020/02/14/12307328.html
-Advertisement-
Play Games

JavaScript中的基本數據類型 在JS中,有6種基本數據類型: 1. string 2. number 3. boolean 4. null 5. undefined 6. Symbol(ES6) 除去這六種基本數據類型以外,其他的所有變數數據類型都是Object。基本類型的操作在JS底層中是 ...


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進行修改

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 簡單介紹了線性佈局和相對佈局,同時分析了xml文件三個固定開頭xmlns:namespace-prefix="namespaceURI" ...
  • 在無序列表ul>li中,無線列表的標誌是出現在各列表前面的圓點。在有序列表ol>li中,前面預設帶有數字,如何修改列表前面的項目符號,只需要通過list-style調整就好,常見的符號有(/*內容註釋部分*/)list-style-type:circle;/*空心圓*/list-style:none... ...
  • 常見的color, font-family, background 等css屬性都能夠設置鏈接的樣式,a鏈接的特殊性在於能夠根據它們所處的狀態來設置它們的樣式。a標簽與人交互的4個狀態屬於偽類狀態切換,常見的鏈接四種狀態為:a:link - 普通的、未被訪問的鏈接a:visited - 用戶已訪問的... ...
  • CSS文本屬性可定義文本的外觀,這是毫無疑問的,其次css可以通過以下屬性改變文字的排版,比方說letter-spacing實現字元間距text-indent: 2em;完成首行縮進2字元word-spacing改變了文字的間距,direction改變文本從左至右的閱讀順序,white-space處... ...
  • 對象的遍歷 對象可以當做數組處理,使用for in var person={}; person.name="cyy"; person.age=25; person.infos=function(){ alert(this.name+" "+this.age); } for(var i in pers ...
  • 前次用 electron-packager 打包成功,這次改用 electron-builder 打包,然後根據項目中實際需要進行選擇使用。 第一步:全局安裝 electron-builder,便於系統通用 npm install -g electron-builder 或 cnpm install ...
  • CSS 字體屬性定義文本的字體系列、大小、加粗、風格(如斜體)和變形(如小型大寫字母)font-family控制字體,由於各個電腦系統安裝的字體不盡相同,但是基本裝有黑體、宋體與微軟雅黑這三款字體,通常這樣寫font-family:"黑體", "宋體","Microsoft YaHei" font-... ...
  • jQuery初學者筆記 一 Mirror王宇陽 by jQuery語法 jQuery語法是通過選取HTML元素,並對選取的元素進行操作 基礎語法: 所有jQuery語句用“$”符號開始 jQuery函數位於一個document ready函數中,我們需要在js中載入該函數文檔 選擇器語法: jQue ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...