在《初中級前端 JavaScript 自測清單 - 1》部分中,和你們簡單過了一遍 JavaScript 基礎知識,沒看過的朋友能夠回顧一下😁javascript
本系列文章是我在咱們團隊內部的「現代 JavaScript 突擊隊」,第一期學習內容爲《現代 JavaScript 教程》系列的第二部分輸出內容,但願這份自測清單,可以幫助你們鞏固知識,溫故知新。前端
本部份內容,以 JavaScript 對象爲主,大體包括如下內容:java
JavaScript 有八種數據額類型,有七種原始類型,它們值只包含一種類型(字符串,數字或其餘),而對象是用來保存鍵值對和更復雜實體。
咱們能夠經過使用帶有可選屬性列表的花括號 **{...}**
來建立對象,一個屬性就是一個鍵值對 {"key" : "value"}
,其中鍵( key
)是一個字符串(或稱屬性名),值( value
)能夠是任何類型。程序員
咱們可使用 2 種方式來建立一個新對象:面試
// 1. 經過「構造函數」建立 let user = new Object(); // 2. 經過「字面量」建立 let user = {};
建立對象時,能夠初始化對象的一些屬性:算法
let user = { name : 'leo', age : 18 }
而後能夠對該對象進行屬性對增刪改查操做:segmentfault
// 增長屬性 user.addr = "China"; // user => {name: "leo", age: 18, addr: "China"} // 刪除屬性 delete user.addr // user => {name: "leo", age: 18} // 修改屬性 user.age = 20; // user => {name: "leo", age: 20} // 查找屬性 user.age; // 20
固然對象的鍵( key
)也能夠是多詞屬性,但必須加引號,使用的時候,必須使用方括號( []
)讀取:數組
let user = { name : 'leo', "my interest" : ["coding", "football", "cycling"] } user["my interest"]; // ["coding", "football", "cycling"] delete user["my interest"];
咱們也能夠在方括號中使用變量,來獲取屬性值:瀏覽器
let key = "name"; let user = { name : "leo", age : 18 } // ok user[key]; // "leo" user[key] = "pingan"; // error user.key; // undefined
建立對象時,能夠在對象字面量中使用方括號,即 計算屬性 :安全
let key = "name"; let inputKey = prompt("請輸入key", "age"); let user = { [key] : "leo", [inputKey] : 18 } // 當用戶在 prompt 上輸入 "age" 時,user 變成下面樣子: // {name: "leo", age: 18}
固然,計算屬性也能夠是表達式:
let key = "name"; let user = { ["my_" + key] : "leo" } user["my_" + key]; // "leo"
實際開發中,能夠將相同的屬性名和屬性值簡寫成更短的語法:
// 本來書寫方式 let getUser = function(name, age){ // ... return { name: name, age: age } } // 簡寫方式 let getUser = function(name, age){ // ... return { name, age } }
也能夠混用:
// 本來書寫方式 let getUser = function(name, age){ // ... return { name: name, age: 18 } } // 簡寫方式 let getUser = function(name, age){ // ... return { name, age: 18 } }
該方法能夠判斷對象的自有屬性和繼承來的屬性是否存在。
let user = {name: "leo"}; "name" in user; //true,自有屬性存在 "age" in user; //false "toString" in user; //true,是一個繼承屬性
該方法只能判斷自有屬性是否存在,對於繼承屬性會返回 false
。
let user = {name: "leo"}; user.hasOwnProperty("name"); //true,自有屬性中有 name user.hasOwnProperty("age"); //false,自有屬性中不存在 age user.hasOwnProperty("toString"); //false,這是一個繼承屬性,但不是自有屬性
該方法能夠判斷對象的自有屬性和繼承屬性。
let user = {name: "leo"}; user.name !== undefined; // true user.age !== undefined; // false user.toString !== undefined // true
該方法存在一個問題,若是屬性的值就是 undefined
的話,該方法不能返回想要的結果:
let user = {name: undefined}; user.name !== undefined; // false,屬性存在,但值是undefined user.age !== undefined; // false user.toString !== undefined; // true
let user = {}; if(user.name) user.name = "pingan"; //若是 name 是 undefine, null, false, " ", 0 或 NaN,它將保持不變 user; // {}
當咱們須要遍歷對象中每個屬性,可使用 for...in
語句來實現
for...in
語句以任意順序遍歷一個對象的除 Symbol
之外的可枚舉屬性。
注意 : for...in
不該該應用在一個數組,其中索引順序很重要。
let user = { name : "leo", age : 18 } for(let k in user){ console.log(k, user[k]); } // name leo // age 18
ES7中新增長的 Object.values()
和Object.entries()
與以前的Object.keys()
相似,返回數組類型。
返回一個數組,成員是參數對象自身的(不含繼承的)全部可遍歷屬性的健名。
let user = { name: "leo", age: 18}; Object.keys(user); // ["name", "age"]
返回一個數組,成員是參數對象自身的(不含繼承的)全部可遍歷屬性的鍵值。
let user = { name: "leo", age: 18}; Object.values(user); // ["leo", 18]
若是參數不是對象,則返回空數組:
Object.values(10); // [] Object.values(true); // []
返回一個數組,成員是參數對象自身的(不含繼承的)全部可遍歷屬性的鍵值對數組。
let user = { name: "leo", age: 18}; Object.entries(user); // [["name","leo"],["age",18]]
手動實現Object.entries()
方法:
// Generator函數實現: function* entries(obj){ for (let k of Object.keys(obj)){ yield [k ,obj[k]]; } } // 非Generator函數實現: function entries (obj){ let arr = []; for(let k of Object.keys(obj)){ arr.push([k, obj[k]]); } return arr; }
該方法返回一個數組,它包含了對象 Obj
全部擁有的屬性(不管是否可枚舉)的名稱。
let user = { name: "leo", age: 18}; Object.getOwnPropertyNames(user); // ["name", "age"]
首先回顧下基本數據類型和引用數據類型:
概念:基本類型值在內存中佔據固定大小,保存在棧內存
中(不包含閉包
中的變量)。
常見包括:undefined,null,Boolean,String,Number,Symbol
概念:引用類型的值是對象,保存在堆內存
中。而棧內存存儲的是對象的變量標識符以及對象在堆內存中的存儲地址(引用),引用數據類型在棧中存儲了指針,該指針指向堆中該實體的起始地址。當解釋器尋找引用值時,會首先檢索其在棧中的地址,取得地址後從堆中得到實體。
常見包括:Object,Array,Date,Function,RegExp等
在棧內存中的數據發生數據變化的時候,系統會自動爲新的變量分配一個新的之值在棧內存中,兩個變量相互獨立,互不影響的。
let user = "leo"; let user1 = user; user1 = "pingan"; console.log(user); // "leo" console.log(user1); // "pingan"
在 JavaScript 中,變量不存儲對象自己,而是存儲其「內存中的地址」,換句話說就是存儲對其的「引用」。
以下面 leo
變量只是保存對user
對象對應引用:
let user = { name: "leo", age: 18}; let leo = user;
其餘變量也能夠引用 user
對象:
let leo1 = user; let leo2 = user;
可是因爲變量保存的是引用,因此當咱們修改變量 leo
leo1
leo2
這些值時,也會改動到引用對象 user
,但當 user
修改,則其餘引用該對象的變量,值都會發生變化:
leo.name = "pingan"; console.log(leo); // {name: "pingan", age: 18} console.log(leo1); // {name: "pingan", age: 18} console.log(leo2); // {name: "pingan", age: 18} console.log(user); // {name: "pingan", age: 18} user.name = "pingan8787"; console.log(leo); // {name: "pingan8787", age: 18} console.log(leo1); // {name: "pingan8787", age: 18} console.log(leo2); // {name: "pingan8787", age: 18} console.log(user); // {name: "pingan8787", age: 18}
這個過程當中涉及變量地址指針指向問題,這裏暫時不展開討論,有興趣的朋友能夠網上查閱相關資料。
當兩個變量引用同一個對象時,它們不管是 ==
仍是 ===
都會返回 true
。
let user = { name: "leo", age: 18}; let leo = user; let leo1 = user; leo == leo1; // true leo === leo1; // true leo == user; // true leo === user; // true
但若是兩個變量是空對象 {}
,則不相等:
let leo1 = {}; let leo2 = {}; leo1 == leo2; // false leo1 === leo2; // false
概念:新的對象複製已有對象中非對象屬性的值和對象屬性的引用。也能夠理解爲:一個新的對象直接拷貝已存在的對象的對象屬性的引用,即淺拷貝。
淺拷貝只對第一層屬性進行了拷貝,當第一層的屬性值是基本數據類型時,新的對象和原對象互不影響,可是若是第一層的屬性值是複雜數據類型,那麼新對象和原對象的屬性值其指向的是同一塊內存地址。
經過示例代碼演示沒有使用淺拷貝場景:
// 示例1 對象原始拷貝 let user = { name: "leo", skill: { JavaScript: 90, CSS: 80}}; let leo = user; leo.name = "leo1"; leo.skill.CSS = 90; console.log(leo.name); // "leo1" console.log(user.name); // "leo1" console.log(leo.skill.CSS); // 90 console.log(user.skill.CSS);// 90 // 示例2 數組原始拷貝 let user = ["leo", "pingan", {name: "pingan8787"}]; let leo = user; leo[0] = "pingan888"; leo[2]["name"] = "pingan999"; console.log(leo[0]); // "pingan888" console.log(user[0]); // "pingan888" console.log(leo[2]["name"]); // "pingan999" console.log(user[2]["name"]); // "pingan999"
從上面示例代碼能夠看出:
因爲對象被直接拷貝,至關於拷貝 引用數據類型 ,因此在新對象修改任何值時,都會改動到源數據。
接下來實現淺拷貝,對比如下。
語法: Object.assign(target, ...sources)
ES6中拷貝對象的方法,接受的第一個參數是拷貝的目標target,剩下的參數是拷貝的源對象sources(能夠是多個)。
詳細介紹,能夠閱讀文檔《MDN Object.assign》。
// 示例1 對象淺拷貝 let user = { name: "leo", skill: { JavaScript: 90, CSS: 80}}; let leo = Object.assign({}, user); leo.name = "leo1"; leo.skill.CSS = 90; console.log(leo.name); // "leo1" ⚠️ 差別! console.log(user.name); // "leo" ⚠️ 差別! console.log(leo.skill.CSS); // 90 console.log(user.skill.CSS);// 90 // 示例2 數組深拷貝 let user = ["leo", "pingan", {name: "pingan8787"}]; let leo = user; leo[0] = "pingan888"; leo[2]["name"] = "pingan999"; console.log(leo[0]); // "pingan888" ⚠️ 差別! console.log(user[0]); // "leo" ⚠️ 差別! console.log(leo[2]["name"]); // "pingan999" console.log(user[2]["name"]); // "pingan999"
從打印結果能夠看出,淺拷貝只是在根屬性(對象的第一層級)建立了一個新的對象,可是對於屬性的值是對象的話只會拷貝一份相同的內存地址。
Object.assign()
使用注意:
Symbol
值的屬性,能夠被Object.assign拷貝;undefined
和null
沒法轉成對象,它們不能做爲Object.assign
參數,可是能夠做爲源對象。Object.assign(undefined); // 報錯 Object.assign(null); // 報錯 Object.assign({}, undefined); // {} Object.assign({}, null); // {} let user = {name: "leo"}; Object.assign(user, undefined) === user; // true Object.assign(user, null) === user; // true
語法: arr.slice([begin[, end]])
slice()
方法返回一個新的數組對象,這一對象是一個由 begin
和 end
決定的原數組的淺拷貝(包括 begin
,不包括end
)。原始數組不會被改變。
詳細介紹,能夠閱讀文檔《MDN Array slice》。
// 示例 數組深拷貝 let user = ["leo", "pingan", {name: "pingan8787"}]; let leo = Array.prototype.slice.call(user); leo[0] = "pingan888"; leo[2]["name"] = "pingan999"; console.log(leo[0]); // "pingan888" ⚠️ 差別! console.log(user[0]); // "leo" ⚠️ 差別! console.log(leo[2]["name"]); // "pingan999" console.log(user[2]["name"]); // "pingan999"
語法: var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])
concat()
方法用於合併兩個或多個數組。此方法不會更改現有數組,而是返回一個新數組。
詳細介紹,能夠閱讀文檔《MDN Array concat》。
let user = [{name: "leo"}, {age: 18}]; let user1 = [{age: 20},{addr: "fujian"}]; let user2 = user.concat(user1); user1[0]["age"] = 25; console.log(user); // [{"name":"leo"},{"age":18}] console.log(user1); // [{"age":25},{"addr":"fujian"}] console.log(user2); // [{"name":"leo"},{"age":18},{"age":25},{"addr":"fujian"}]
Array.prototype.concat
也是一個淺拷貝,只是在根屬性(對象的第一層級)建立了一個新的對象,可是對於屬性的值是對象的話只會拷貝一份相同的內存地址。
語法: var cloneObj = { ...obj };
擴展運算符也是淺拷貝,對於值是對象的屬性沒法徹底拷貝成2個不一樣對象,可是若是屬性都是基本類型的值的話,使用擴展運算符也是優點方便的地方。
let user = { name: "leo", skill: { JavaScript: 90, CSS: 80}}; let leo = {...user}; leo.name = "leo1"; leo.skill.CSS = 90; console.log(leo.name); // "leo1" ⚠️ 差別! console.log(user.name); // "leo" ⚠️ 差別! console.log(leo.skill.CSS); // 90 console.log(user.skill.CSS);// 90
實現原理:新的對象複製已有對象中非對象屬性的值和對象屬性的引用,也就是說對象屬性並不複製到內存。
function cloneShallow(source) { let target = {}; for (let key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } return target; }
for...in語句以任意順序遍歷一個對象自有的、繼承的、可枚舉的
、非Symbol的屬性。對於每一個不一樣的屬性,語句都會被執行。
該函數返回值爲布爾值,全部繼承了 Object 的對象都會繼承到 hasOwnProperty
方法,和 in
運算符不一樣,該函數會忽略掉那些從原型鏈上繼承到的屬性和自身屬性。
語法:obj.hasOwnProperty(prop)
prop
是要檢測的屬性字符串名稱或者Symbol
。
複製變量值,對於引用數據,則遞歸至基本類型後,再複製。深拷貝後的對象與原來的對象徹底隔離,互不影響,對一個對象的修改並不會影響另外一個對象。
其原理是把一個對象序列化成爲一個JSON字符串,將對象的內容轉換成字符串的形式再保存在磁盤上,再用JSON.parse()
反序列化將JSON字符串變成一個新的對象。
let user = { name: "leo", skill: { JavaScript: 90, CSS: 80}}; let leo = JSON.parse(JSON.stringify(user)); leo.name = "leo1"; leo.skill.CSS = 90; console.log(leo.name); // "leo1" ⚠️ 差別! console.log(user.name); // "leo" ⚠️ 差別! console.log(leo.skill.CSS); // 90 ⚠️ 差別! console.log(user.skill.CSS);// 80 ⚠️ 差別!
JSON.stringify()
使用注意:
undefined
, symbol
則通過 JSON.stringify()
`序列化後的JSON字符串中這個鍵值對會消失;Date
引用類型會變成字符串;RegExp
引用類型會變成空對象;NaN
、 Infinity
和 -Infinity
,則序列化的結果會變成 null
;obj[key] = obj
)。核心思想是遞歸,遍歷對象、數組直到裏邊都是基本數據類型,而後再去複製,就是深度拷貝。 實現代碼:
const isObject = obj => typeof obj === 'object' && obj != null; function cloneDeep(source) { if (!isObject(source)) return source; // 非對象返回自身 const target = Array.isArray(source) ? [] : {}; for(var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { if (isObject(source[key])) { target[key] = cloneDeep(source[key]); // 注意這裏 } else { target[key] = source[key]; } } } return target; }
該方法缺陷: 遇到循環引用,會陷入一個循環的遞歸過程,從而致使爆棧。
其餘寫法,能夠閱讀《如何寫出一個驚豔面試官的深拷貝?》 。
淺拷貝:將對象的每一個屬性進行依次複製,可是當對象的屬性值是引用類型時,實質複製的是其引用,當引用指向的值改變時也會跟着變化。
深拷貝:複製變量值,對於引用數據,則遞歸至基本類型後,再複製。深拷貝後的對象與原來的對象徹底隔離,互不影響,對一個對象的修改並不會影響另外一個對象。
深拷貝和淺拷貝是針對複雜數據類型來講的,淺拷貝只拷貝一層,而深拷貝是層層拷貝。
垃圾回收(Garbage Collection,縮寫爲GC))是一種自動的存儲器管理機制。當某個程序佔用的一部份內存空間再也不被這個程序訪問時,這個程序會藉助垃圾回收算法向操做系統歸還這部份內存空間。垃圾回收器能夠減輕程序員的負擔,也減小程序中的錯誤。垃圾回收最先起源於LISP語言。
目前許多語言如Smalltalk、Java、C#和D語言都支持垃圾回收器,咱們熟知的 JavaScript 具備自動垃圾回收機制。
在 JavaScript 中,原始類型的數據被分配到棧空間中,引用類型的數據會被分配到堆空間中。
當函數 showName
調用完成後,經過下移 ESP(Extended Stack Pointer)指針,來銷燬 showName
函數,以後調用其餘函數時,將覆蓋掉舊內存,存放另外一個函數的執行上下文,實現垃圾回收。
圖片來自《瀏覽器工做原理與實踐》
堆中數據垃圾回收策略的基礎是:代際假說(The Generational Hypothesis)。即:
這兩個特色不只僅適用於 JavaScript,一樣適用於大多數的動態語言,如 Java、Python 等。
V8 引擎將堆空間分爲新生代(存放生存時間短的對象)和老生代(存放生存時間長的對象)兩個區域,並使用不一樣的垃圾回收器。
無論是哪一種垃圾回收器,都使用相同垃圾回收流程:標記活動對象和非活動對象,回收非活動對象的內存,最後內存整理。
**
使用 Scavenge 算法處理,將新生代空間對半分爲兩個區域,一個對象區域,一個空閒區域。
圖片來自《瀏覽器工做原理與實踐》
執行流程:
固然,這也存在一些問題:若複製操做的數據較大則影響清理效率。
JavaScript 引擎的解決方式是:將新生代區域設置得比較小,並採用對象晉升策略(通過兩次回收仍存活的對象,會被移動到老生區),避免由於新生代區域較小引發存活對象裝滿整個區域的問題。
分爲:標記 - 清除(Mark-Sweep)算法,和標記 - 整理(Mark-Compact)算法。
a)標記 - 清除(Mark-Sweep)算法
過程:
圖片來自《瀏覽器工做原理與實踐》
b)標記 - 整理(Mark-Compact)算法
過程:
圖片來自《瀏覽器工做原理與實踐》
具體介紹可閱讀 《MDN 方法的定義》 。
將做爲對象屬性的方法稱爲「對象方法」,以下面 user
對象的 say
方法:
let user = {}; let say = function(){console.log("hello!")}; user.say = say; // 賦值到對象上 user.say(); // "hello!"
也可使用更加簡潔的方法:
let user = { say: function(){} // 簡寫爲 say (){console.log("hello!")} // ES8 async 方法 async say (){/.../} } user.say();
固然對象方法的名稱,還支持計算的屬性名稱做爲方法名:
const hello = "Hello"; let user = { ['say' + hello](){console.log("hello!")} } user['say' + hello](); // "hello!"
另外須要注意的是:全部方法定義不是構造函數,若是您嘗試實例化它們,將拋出TypeError
。
let user = { say(){}; } new user.say; // TypeError: user.say is not a constructor
當對象方法須要使用對象中的屬性,可使用 this
關鍵字:
let user = { name : 'leo', say(){ console.log(`hello ${this.name}`)} } user.say(); // "hello leo"
當代碼 user.say()
執行過程當中, this
指的是 user
對象。固然也能夠直接使用變量名 user
來引用 say()
方法:
let user = { name : 'leo', say(){ console.log(`hello ${user.name}`)} } user.say(); // "hello leo"
可是這樣並不安全,由於 user
對象可能賦值給另一個變量,而且將其餘值賦值給 user
對象,就可能致使報錯:
let user = { name : 'leo', say(){ console.log(`hello ${user.name}`)} } let leo = user; user = null; leo.say(); // Uncaught TypeError: Cannot read property 'name' of null
但將 user.name
改爲 this.name
代碼便正常運行。
this
的值是在 代碼運行時計算出來 的,它的值取決於代碼上下文:
let user = { name: "leo"}; let admin = {name: "pingan"}; let say = function (){ console.log(`hello ${this.name}`) }; user.fun = say; admin.fun = say; // 函數內部 this 是指「點符號前面」的對象 user.fun(); // "hello leo" admin.fun(); // "hello pingan" admin['fun'](); // "hello pingan"
規則:若是 obj.fun()
被調用,則 this
在 fun
函數調用期間是 obj
,因此上面的 this
先是 user
,而後是 admin
。
可是在全局環境中,不管是否開啓嚴格模式, this
都指向全局對象
console.log(this == window); // true let a = 10; this.b = 10; a === this.b; // true
箭頭函數比較特別,沒有本身的 this
,若是有引用 this
的話,則指向外部正常函數,下面例子中, this
指向 user.say()
方法:
let user = { name : 'leo', say : () => { console.log(`hello ${this.name}`); }, hello(){ let fun = () => console.log(`hello ${this.name}`); fun(); } } user.say(); // hello => say() 外部函數是 window user.hello(); // hello leo => fun() 外部函數是 hello
詳細能夠閱讀《js基礎-關於call,apply,bind的一切》 。
當咱們想把 this
值綁定到另外一個環境中,就可使用 call
/ apply
/ bind
方法實現:
var user = { name: 'leo' }; var name = 'pingan'; function fun(){ return console.log(this.name); // this 的值取決於函數調用方式 } fun(); // "pingan" fun.call(user); // "leo" fun.apply(user); // "leo"
注意:這裏的 var name = 'pingan';
須要使用 var
來聲明,使用 let
的話, window
上將沒有 name
變量。
三者語法以下:
fun.call(thisArg, param1, param2, ...) fun.apply(thisArg, [param1,param2,...]) fun.bind(thisArg, param1, param2, ...)
構造函數的做用在於 實現可重用的對象建立代碼 。
一般,對於構造函數有兩個約定:
new
運算符執行。new
運算符建立一個用戶定義的對象類型的實例或具備構造函數的內置對象的實例。
語法以下:
new constructor[([arguments])]
參數以下:
constructor
一個指定對象實例的類型的類或函數。arguments
一個用於被 constructor
調用的參數列表。舉個簡單示例:
function User (name){ this.name = name; this.isAdmin = false; } const leo = new User('leo'); console.log(leo.name, leo.isAdmin); // "leo" false
當一個函數被使用 new
運算符執行時,它按照如下步驟:
this
。this
,爲其添加新的屬性。this
的值。之前面 User
方法爲例:
function User(name) { // this = {};(隱式建立) // 添加屬性到 this this.name = name; this.isAdmin = false; // return this;(隱式返回) } const leo = new User('leo'); console.log(leo.name, leo.isAdmin); // "leo" false
當咱們執行 new User('leo')
時,發生如下事情:
User.prototype
的新對象被建立;User
,並將 this
綁定到新建立的對象;new
表達式的結果。若是構造函數沒有顯式返回一個對象,則使用步驟1建立的對象。須要注意:
new User
等同於 new User()
,只是沒有指定參數列表,即 User
不帶參數的狀況;let user = new User; // <-- 沒有參數 // 等同於 let user = new User();
new
運算符運行。在構造函數中,也能夠將方法綁定到 this
上:
function User (name){ this.name = name; this.isAdmin = false; this.sayHello = function(){ console.log("hello " + this.name); } } const leo = new User('leo'); console.log(leo.name, leo.isAdmin); // "leo" false leo.sayHello(); // "hello leo"
詳細介紹能夠查看 《MDN 可選鏈操做符》 。
在實際開發中,經常出現下面幾種報錯狀況:
// 1. 對象中不存在指定屬性 const leo = {}; console.log(leo.name.toString()); // Uncaught TypeError: Cannot read property 'toString' of undefined // 2. 使用不存在的 DOM 節點屬性 const dom = document.getElementById("dom").innerHTML; // Uncaught TypeError: Cannot read property 'innerHTML' of null
在可選鏈 ?.
出現以前,咱們會使用短路操做 &&
運算符來解決該問題:
const leo = {}; console.log(leo && leo.name && leo.name.toString()); // undefined
這種寫法的缺點就是 太麻煩了 。
可選鏈 ?.
是一種 訪問嵌套對象屬性的防錯誤方法 。即便中間的屬性不存在,也不會出現錯誤。
若是可選鏈 ?.
前面部分是 undefined
或者 null
,它會中止運算並返回 undefined
。
語法:
obj?.prop obj?.[expr] arr?.[index] func?.(args)
**
咱們改造前面示例代碼:
// 1. 對象中不存在指定屬性 const leo = {}; console.log(leo?.name?.toString()); // undefined // 2. 使用不存在的 DOM 節點屬性 const dom = document?.getElementById("dom")?.innerHTML; // undefined
可選鏈雖然好用,但須要注意如下幾點:
咱們應該只將 ?.
使用在一些屬性或方法能夠不存在的地方,以上面示例代碼爲例:
const leo = {}; console.log(leo.name?.toString());
這樣寫會更好,由於 leo
對象是必須存在,而 name
屬性則可能不存在。
?.
以前的變量必須已聲明;在可選鏈 ?.
以前的變量必須使用 let/const/var
聲明,不然會報錯:
leo?.name; // Uncaught ReferenceError: leo is not defined
let object = {}; object?.property = 1; // Uncaught SyntaxError: Invalid left-hand side in assignment
let arrayItem = arr?.[42];
須要說明的是 ?.
是一個特殊的語法結構,而不是一個運算符,它還能夠與其 ()
和 []
一塊兒使用:
?.()
用於調用一個可能不存在的函數,好比:
let user1 = { admin() { alert("I am admin"); } } let user2 = {}; user1.admin?.(); // I am admin user2.admin?.();
?.()
會檢查它左邊的部分:若是 admin 函數存在,那麼就調用運行它(對於 user1
)。不然(對於 user2
)運算中止,沒有錯誤。
?.[]
容許從一個可能不存在的對象上安全地讀取屬性。
let user1 = { firstName: "John" }; let user2 = null; // 假設,咱們不能受權此用戶 let key = "firstName"; alert( user1?.[key] ); // John alert( user2?.[key] ); // undefined alert( user1?.[key]?.something?.not?.existing); // undefined
?.
語法總結可選鏈 ?.
語法有三種形式:
obj?.prop
—— 若是 obj
存在則返回 obj.prop
,不然返回 undefined
。obj?.[prop]
—— 若是 obj
存在則返回 obj[prop]
,不然返回 undefined
。obj?.method()
—— 若是 obj
存在則調用 obj.method()
,不然返回 undefined
。正如咱們所看到的,這些語法形式用起來都很簡單直接。?.
檢查左邊部分是否爲 null/undefined
,若是不是則繼續運算。?.
鏈使咱們可以安全地訪問嵌套屬性。
規範規定,JavaScript 中對象的屬性只能爲 字符串類型 或者 Symbol類型 ,畢竟咱們也只見過這兩種類型。
ES6引入Symbol
做爲一種新的原始數據類型,表示獨一無二的值,主要是爲了防止屬性名衝突。
ES6以後,JavaScript一共有其中數據類型:Symbol
、undefined
、null
、Boolean
、String
、Number
、Object
。
簡單使用:
let leo = Symbol(); typeof leo; // "symbol"
Symbol 支持傳入參數做爲 Symbol 名,方便代碼調試:
**
let leo = Symbol("leo");
Symbol
函數不能用new
,會報錯。因爲Symbol
是一個原始類型,不是對象,因此不能添加屬性,它是相似於字符串的數據類型。
let leo = new Symbol() // Uncaught TypeError: Symbol is not leo constructor
Symbol
都是不相等的,即便參數相同。// 沒有參數 let leo1 = Symbol(); let leo2 = Symbol(); leo1 === leo2; // false // 有參數 let leo1 = Symbol('leo'); let leo2 = Symbol('leo'); leo1 === leo2; // false
Symbol
不能與其餘類型的值計算,會報錯。let leo = Symbol('hello'); leo + " world!"; // 報錯 `${leo} world!`; // 報錯
Symbol
不能自動轉換爲字符串,只能顯式轉換。let leo = Symbol('hello'); alert(leo); // Uncaught TypeError: Cannot convert a Symbol value to a string String(leo); // "Symbol(hello)" leo.toString(); // "Symbol(hello)"
Symbol
能夠轉換爲布爾值,但不能轉爲數值:let a1 = Symbol(); Boolean(a1); !a1; // false Number(a1); // TypeError a1 + 1 ; // TypeError
Symbol
屬性不參與 for...in/of
循環。let id = Symbol("id"); let user = { name: "Leo", age: 30, [id]: 123 }; for (let key in user) console.log(key); // name, age (no symbols) // 使用 Symbol 任務直接訪問 console.log( "Direct: " + user[id] );
在對象字面量中使用 Symbol
做爲屬性名時,須要使用 方括號 ( []
),如 [leo]: "leo"
。
好處:防止同名屬性,還有防止鍵被改寫或覆蓋。
let leo = Symbol(); // 寫法1 let user = {}; user[leo] = 'leo'; // 寫法2 let user = { [leo] : 'leo' } // 寫法3 let user = {}; Object.defineProperty(user, leo, {value : 'leo' }); // 3種寫法 結果相同 user[leo]; // 'leo'
須要注意 :Symbol做爲對象屬性名時,不能用點運算符,而且必須放在方括號內。
let leo = Symbol(); let user = {}; // 不能用點運算 user.leo = 'leo'; user[leo] ; // undefined user['leo'] ; // 'leo' // 必須放在方括號內 let user = { [leo] : function (text){ console.log(text); } } user[leo]('leo'); // 'leo' // 上面等價於 更簡潔 let user = { [leo](text){ console.log(text); } }
經常還用於建立一組常量,保證全部值不相等:
let user = {}; user.list = { AAA: Symbol('Leo'), BBB: Symbol('Robin'), CCC: Symbol('Pingan') }
魔術字符串:指代碼中屢次出現,強耦合的字符串或數值,應該避免,而使用含義清晰的變量代替。
function fun(name){ if(name == 'leo') { console.log('hello'); } } fun('leo'); // 'hello' 爲魔術字符串
常使用變量,消除魔術字符串:
let obj = { name: 'leo' }; function fun(name){ if(name == obj.name){ console.log('hello'); } } fun(obj.name); // 'hello'
使用Symbol
消除強耦合,使得不需關係具體的值:
let obj = { name: Symbol() }; function fun (name){ if(name == obj.name){ console.log('hello'); } } fun(obj.name); // 'hello'
Symbol做爲屬性名遍歷,不出如今for...in
、for...of
循環,也不被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。
let leo = Symbol('leo'), robin = Symbol('robin'); let user = { [leo]:'18', [robin]:'28' } for(let k of Object.values(user)){console.log(k)} // 無輸出 let user = {}; let leo = Symbol('leo'); Object.defineProperty(user, leo, {value: 'hi'}); for(let k in user){ console.log(k); // 無輸出 } Object.getOwnPropertyNames(user); // [] Object.getOwnPropertySymbols(user); // [Symbol(leo)]
Object.getOwnPropertySymbols
方法返回一個數組,包含當前對象全部用作屬性名的Symbol值。
let user = {}; let leo = Symbol('leo'); let pingan = Symbol('pingan'); user[leo] = 'hi leo'; user[pingan] = 'hi pingan'; let obj = Object.getOwnPropertySymbols(user); obj; // [Symbol(leo), Symbol(pingan)]
另外可使用Reflect.ownKeys
方法能夠返回全部類型的鍵名,包括常規鍵名和 Symbol 鍵名。
let user = { [Symbol('leo')]: 1, age : 2, address : 3, } Reflect.ownKeys(user); // ['age', 'address',Symbol('leo')]
因爲Symbol值做爲名稱的屬性不被常規方法遍歷獲取,所以經常使用於定義對象的一些非私有,且內部使用的方法。
用於重複使用一個Symbol值,接收一個字符串做爲參數,若存在用此參數做爲名稱的Symbol值,返回這個Symbol,不然新建並返回以這個參數爲名稱的Symbol值。
let leo = Symbol.for('leo'); let pingan = Symbol.for('pingan'); leo === pingan; // true
Symbol()
和 Symbol.for()
區別:
Symbol.for('leo') === Symbol.for('leo'); // true Symbol('leo') === Symbol('leo'); // false
用於返回一個已使用的Symbol類型的key:
let leo = Symbol.for('leo'); Symbol.keyFor(leo); // 'leo' let leo = Symbol('leo'); Symbol.keyFor(leo); // undefined
ES6提供11個內置的Symbol值,指向語言內部使用的方法:
當其餘對象使用instanceof
運算符,判斷是否爲該對象的實例時,會調用這個方法。好比,foo instanceof Foo
在語言內部,實際調用的是Foo[Symbol.hasInstance](foo)
。
class P { [Symbol.hasInstance](a){ return a instanceof Array; } } [1, 2, 3] instanceof new P(); // true
P是一個類,new P()會返回一個實例,該實例的Symbol.hasInstance
方法,會在進行instanceof
運算時自動調用,判斷左側的運算子是否爲Array
的實例。
值爲布爾值,表示該對象用於Array.prototype.concat()
時,是否能夠展開。
let a = ['aa','bb']; ['cc','dd'].concat(a, 'ee'); // ['cc', 'dd', 'aa', 'bb', 'ee'] a[Symbol.isConcatSpreadable]; // undefined let b = ['aa','bb']; b[Symbol.isConcatSpreadable] = false; ['cc','dd'].concat(b, 'ee'); // ['cc', 'dd',[ 'aa', 'bb'], 'ee']
指向一個構造函數,在建立衍生對象時會使用,使用時須要用get
取值器。
class P extends Array { static get [Symbol.species](){ return this; } }
解決下面問題:
// 問題: b應該是 Array 的實例,其實是 P 的實例 class P extends Array{} let a = new P(1,2,3); let b = a.map(x => x); b instanceof Array; // true b instanceof P; // true // 解決: 經過使用 Symbol.species class P extends Array { static get [Symbol.species]() { return Array; } } let a = new P(); let b = a.map(x => x); b instanceof P; // false b instanceof Array; // true
當執行str.match(myObject)
,傳入的屬性存在時會調用,並返回該方法的返回值。
class P { [Symbol.match](string){ return 'hello world'.indexOf(string); } } 'h'.match(new P()); // 0
當該對象被String.prototype.replace
方法調用時,會返回該方法的返回值。
let a = {}; a[Symbol.replace] = (...s) => console.log(s); 'Hello'.replace(a , 'World') // ["Hello", "World"]
當該對象被String.prototype.search
方法調用時,會返回該方法的返回值。
class P { constructor(val) { this.val = val; } [Symbol.search](s){ return s.indexOf(this.val); } } 'hileo'.search(new P('leo')); // 2
當該對象被String.prototype.split
方法調用時,會返回該方法的返回值。
// 從新定義了字符串對象的split方法的行爲 class P { constructor(val) { this.val = val; } [Symbol.split](s) { let i = s.indexOf(this.val); if(i == -1) return s; return [ s.substr(0, i), s.substr(i + this.val.length) ] } } 'helloworld'.split(new P('hello')); // ["hello", ""] 'helloworld'.split(new P('world')); // ["", "world"] 'helloworld'.split(new P('leo')); // "helloworld"
對象進行for...of
循環時,會調用Symbol.iterator
方法,返回該對象的默認遍歷器。
class P { *[Symbol.interator]() { let i = 0; while(this[i] !== undefined ) { yield this[i]; ++i; } } } let a = new P(); a[0] = 1; a[1] = 2; for (let k of a){ console.log(k); }
該對象被轉爲原始類型的值時,會調用這個方法,返回該對象對應的原始類型值。調用時,須要接收一個字符串參數,表示當前運算模式,運算模式有:
let obj = { [Symbol.toPrimitive](hint) { switch (hint) { case 'number': return 123; case 'string': return 'str'; case 'default': return 'default'; default: throw new Error(); } } }; 2 * obj // 246 3 + obj // '3default' obj == 'default' // true String(obj) // 'str'
在該對象上面調用Object.prototype.toString
方法時,若是這個屬性存在,它的返回值會出如今toString
方法返回的字符串之中,表示對象的類型。也就是說,這個屬性能夠用來定製[object Object
]或[object Array]
中object
後面的那個字符串。
// 例一 ({[Symbol.toStringTag]: 'Foo'}.toString()) // "[object Foo]" // 例二 class Collection { get [Symbol.toStringTag]() { return 'xxx'; } } let x = new Collection(); Object.prototype.toString.call(x) // "[object xxx]"
該對象指定了使用with關鍵字時,哪些屬性會被with環境排除。
// 沒有 unscopables 時 class MyClass { foo() { return 1; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); // 1 } // 有 unscopables 時 class MyClass { foo() { return 1; } get [Symbol.unscopables]() { return { foo: true }; } } var foo = function () { return 2; }; with (MyClass.prototype) { foo(); // 2 }
上面代碼經過指定Symbol.unscopables
屬性,使得with
語法塊不會在當前做用域尋找foo
屬性,即foo
將指向外層做用域的變量。
前面複習到字符串、數值、布爾值等的轉換,可是沒有講到對象的轉換規則,這部分就一塊兒看看:。
須要記住幾個規則:
true
,而且不存在轉換爲布爾值的操做,只有字符串和數值轉換有。Date
對象能夠相減,如 date1 - date2
結果爲兩個時間的差值。alert(obj)
這種形式。固然咱們可使用特殊的對象方法,對字符串和數值轉換進行微調。下面介紹三個類型(hint)轉換狀況:
對象到字符串的轉換,當咱們對指望一個字符串的對象執行操做時,如 「alert」:
// 輸出 alert(obj); // 將對象做爲屬性鍵 anotherObj[obj] = 123;
對象到數字的轉換,例如當咱們進行數學運算時:
// 顯式轉換 let num = Number(obj); // 數學運算(除了二進制加法) let n = +obj; // 一元加法 let delta = date1 - date2; // 小於/大於的比較 let greater = user1 > user2;
少數狀況下,當運算符「不肯定」指望值類型時。
例如,二進制加法 +
可用於字符串(鏈接),也能夠用於數字(相加),因此字符串和數字這兩種類型均可以。所以,當二元加法獲得對象類型的參數時,它將依據 "default"
來對其進行轉換。
此外,若是對象被用於與字符串、數字或 symbol 進行 ==
比較,這時到底應該進行哪一種轉換也不是很明確,所以使用 "default"
。
// 二元加法使用默認 hint let total = obj1 + obj2; // obj == number 使用默認 hint if (user == 1) { ... };
爲了進行轉換,JavaScript 嘗試查找並調用三個對象方法:
obj[Symbol.toPrimitive](hint)
—— 帶有 symbol 鍵 Symbol.toPrimitive
(系統 symbol)的方法,若是這個方法存在的話,"string"
—— 嘗試 obj.toString()
和 obj.valueOf()
,不管哪一個存在。"number"
或 "default"
—— 嘗試 obj.valueOf()
和 obj.toString()
,不管哪一個存在。詳細介紹可閱讀《MDN | Symbol.toPrimitive》 。Symbol.toPrimitive
是一個內置的 Symbol 值,它是做爲對象的函數值屬性存在的,當一個對象轉換爲對應的原始值時,會調用此函數。
簡單示例介紹:
let user = { name: "Leo", money: 9999, [Symbol.toPrimitive](hint) { console.log(`hint: ${hint}`); return hint == "string" ? `{name: "${this.name}"}` : this.money; } }; alert(user); // 控制檯:hint: string 彈框:{name: "John"} alert(+user); // 控制檯:hint: number 彈框:9999 alert(user + 1); // 控制檯:hint: default 彈框:10000
toString
/ valueOf
是兩個比較早期的實現轉換的方法。當沒有 Symbol.toPrimitive
,那麼 JavaScript 將嘗試找到它們,而且按照下面的順序進行嘗試:
toString -> valueOf
。valueOf -> toString
。這兩個方法必須返回一個原始值。若是 toString
或 valueOf
返回了一個對象,那麼返回值會被忽略。默認狀況下,普通對象具備 toString
和 valueOf
方法:
toString
方法返回一個字符串 "[object Object]"
。valueOf
方法返回對象自身。簡單示例介紹:
const user = {name: "Leo"}; alert(user); // [object Object] alert(user.valueOf() === user); // true
咱們也能夠結合 toString
/ valueOf
實現前面第 5 點介紹的 user
對象:
let user = { name: "Leo", money: 9999, // 對於 hint="string" toString() { return `{name: "${this.name}"}`; }, // 對於 hint="number" 或 "default" valueOf() { return this.money; } }; alert(user); // 控制檯:hint: string 彈框:{name: "John"} alert(+user); // 控制檯:hint: number 彈框:9999 alert(user + 1); // 控制檯:hint: default 彈框:10000
本文做爲《初中級前端 JavaScript 自測清單》第二部分,介紹的內容以 JavaScript 對象爲主,其中有讓我眼前一亮的知識點,如 Symbol.toPrimitive
方法。我也但願這個清單能幫助你們自測本身的 JavaScript 水平並查缺補漏,溫故知新。