您現在的位置是:首頁 > 旅遊
1.1萬字|前端打工人 JavaScript 自測清單
萬字大寫有幾種寫法
「來源: |Vue中文社群 ID:vue_fe」
本部分內容,以
「JavaScript 物件」
為主,大致包括以下內容:
一、物件
JavaScript 有八種資料額型別,有七種原始型別,它們值只包含一種型別(字串,數字或其他),而物件是用來
「儲存鍵值對和更復雜實體。」
我們可以透過使用帶有可選
「屬性列表」
的花括號 **{。。。}** 來建立物件,一個屬性就是一個鍵值對 {“key” : “value”} ,其中鍵( key )是一個字串(或稱屬性名),值( value )可以是任何型別。
1。 建立物件
我們可以使用 2 種方式來建立一個新物件:
// 1。 透過“建構函式”建立
let user = newObject();
// 2。 透過“字面量”建立
let user = {};
2。 物件文字和屬性
建立物件時,可以初始化物件的一些屬性:
let user = {
name : ‘leo’,
age : 18
}
然後可以對該物件進行屬性對
「增刪改查」
操作:
// 增加屬性
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
3。 方括號的使用
當然物件的鍵( 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
4。 計算屬性
建立物件時,可以在物件字面量中使用方括號,即
「計算屬性」
:
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”
5。 屬性名簡寫
實際開發中,可以將相同的屬性名和屬性值簡寫成更短的語法:
// 原本書寫方式
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
}
}
6。 物件屬性存在性檢測
6。1 使用 in 關鍵字
該方法可以判斷
「物件的自有屬性和繼承來的屬性」
是否存在。
let user = {name: “leo”};
“name”in user; //true,自有屬性存在
“age”in user; //false
“toString”in user; //true,是一個繼承屬性
6。2使用物件的 hasOwnProperty() 方法。
該方法只能判斷
「自有屬性」
是否存在,對於
「繼承屬性」
會返回 false 。
let user = {name: “leo”};
user。hasOwnProperty(“name”); //true,自有屬性中有 name
user。hasOwnProperty(“age”); //false,自有屬性中不存在 age
user。hasOwnProperty(“toString”); //false,這是一個繼承屬性,但不是自有屬性
6。3 用 undefined 判斷
該方法可以判斷物件的
「自有屬性和繼承屬性」
。
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
6。4 在條件語句中直接判斷
let user = {};
if(user。name) user。name = “pingan”;
//如果 name 是 undefine, null, false, “ ”, 0 或 NaN,它將保持不變
user; // {}
7。 物件迴圈遍歷
當我們需要遍歷物件中每一個屬性,可以使用 for。。。in 語句來實現
7。1 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
7。2 ES7 新增方法
ES7中新增加的 Object。values()和Object。entries()與之前的Object。keys()類似,返回陣列型別。
1。 Object。keys()
返回一個數組,成員是引數物件自身的(不含繼承的)所有
「可遍歷屬性」
的健名。
let user = { name: “leo”, age: 18};
Object。keys(user); // [“name”, “age”]
2。 Object。values()
返回一個數組,成員是引數物件自身的(不含繼承的)所有可遍歷屬性的鍵值。
let user = { name: “leo”, age: 18};
Object。values(user); // [“leo”, 18]
如果引數不是物件,則返回空陣列:
Object。values(10); // []
Object。values(true); // []
3。 Object。entries()
返回一個數組,成員是引數物件自身的(不含繼承的)所有
「可遍歷屬性」
的鍵值對陣列。
let user = { name: “leo”, age: 18};
Object。entries(user);
// [[“name”,“leo”],[“age”,18]]
手動實現Object。entries()方法:
// Generator函式實現:
function* entries(obj){
for (let k ofObject。keys(obj)){
yield [k ,obj[k]];
}
}
// 非Generator函式實現:
functionentries (obj){
let arr = [];
for(let k ofObject。keys(obj)){
arr。push([k, obj[k]]);
}
return arr;
}
4。 Object。getOwnPropertyNames(Obj)
該方法返回一個數組,它包含了物件 Obj 所有擁有的屬性(
「無論是否可列舉」
)的名稱。
let user = { name: “leo”, age: 18};
Object。getOwnPropertyNames(user);
// [“name”, “age”]
二、物件複製
參考文章《搞不懂JS中賦值·淺複製·深複製的請看這裡》
1。 賦值操作
首先回顧下基本資料型別和引用資料型別:
基本型別
概念:基本型別值在記憶體中佔據固定大小,儲存在棧記憶體中(不包含閉包中的變數)。常見包括:undefined,null,Boolean,String,Number,Symbol
引用型別
概念:引用型別的值是物件,儲存在堆記憶體中。而棧記憶體儲存的是物件的變數識別符號以及物件在堆記憶體中的儲存地址(引用),引用資料型別在棧中儲存了指標,該指標指向堆中該實體的起始地址。當直譯器尋找引用值時,會首先檢索其在棧中的地址,取得地址後從堆中獲得實體。常見包括:Object,Array,Date,Function,RegExp等
1。1 基本資料型別賦值
在棧記憶體中的資料發生資料變化的時候,系統會自動為新的變數分配一個新的之值在棧記憶體中,兩個變數相互獨立,互不影響的。
let user = “leo”;
let user1 = user;
user1 = “pingan”;
console。log(user); // “leo”
console。log(user1); // “pingan”
1。2 引用資料型別賦值
在 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}
這個過程中涉及變數地址指標指向問題,這裡暫時不展開討論,有興趣的朋友可以網上查閱相關資料。
2。 物件比較
當兩個變數引用同一個物件時,它們無論是 == 還是 === 都會返回 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
3。 淺複製
3。1 概念
概念:
「新的物件複製已有物件中非物件屬性的值和物件屬性的引用」
。也可以理解為:
「一個新的物件直接複製已存在的物件的物件屬性的引用」
,即淺複製。
淺複製
「只對第一層屬性進行了複製」
,當第一層的屬性值是基本資料型別時,新的物件和原物件互不影響,但是如果第一層的屬性值是複雜資料型別,那麼新物件和原物件的屬性值其指向的是同一塊記憶體地址。
透過示例程式碼演示沒有使用淺複製場景:
// 示例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”
從上面示例程式碼可以看出:由於物件被直接複製,相當於複製
「引用資料型別」
,所以在新物件修改任何值時,都會改動到源資料。
接下來實現淺複製,對比以下。
3。2 實現淺複製
1。 Object。assign()
語法: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
2。 Array。prototype。slice()
語法: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”
3。 Array。prototype。concat()
語法: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 也是一個淺複製,只是在根屬性(物件的第一層級)建立了一個新的物件,但是對於屬性的值是物件的話只會複製一份相同的記憶體地址。
4。 拓展運算子(。。。)
語法: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
3。3 手寫淺複製
實現原理:新的物件複製已有物件中非物件屬性的值和物件屬性的
「引用」
,也就是說物件屬性並不複製到記憶體。
functioncloneShallow(source) {
let target = {};
for (let key in source) {
if (Object。prototype。hasOwnProperty。call(source, key)) {
target[key] = source[key];
}
}
return target;
}
「for in」
for。。。in語句以任意順序遍歷一個物件自有的、繼承的、可列舉的、非Symbol的屬性。對於每個不同的屬性,語句都會被執行。
「hasOwnProperty」
該函式返回值為布林值,所有繼承了 Object 的物件都會繼承到 hasOwnProperty 方法,和 in 運算子不同,該函式會忽略掉那些從原型鏈上繼承到的屬性和自身屬性。語法:obj。hasOwnProperty(prop)prop 是要檢測的屬性
「字串名稱」
或者Symbol。
4。 深複製
4。1 概念
複製變數值,對於引用資料,則遞迴至基本型別後,再複製。深複製後的物件
「與原來的物件完全隔離」
,互不影響,對一個物件的修改並不會影響另一個物件。
4。2 實現深複製
1。 JSON。parse(JSON。stringify())
其原理是把一個物件序列化成為一個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 )。
2。 第三方庫
4。3 手寫深複製
核心思想是
「遞迴」
,遍歷物件、陣列直到裡邊都是基本資料型別,然後再去複製,就是深度複製。實現程式碼:
const isObject = obj =>typeof obj === ‘object’ && obj != null;
functioncloneDeep(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;
}
該方法缺陷:遇到迴圈引用,會陷入一個迴圈的遞迴過程,從而導致爆棧。其他寫法,可以閱讀《如何寫出一個驚豔面試官的深複製?》 。
5。 小結
「淺複製」
:將物件的每個屬性進行依次複製,但是當物件的屬性值是引用型別時,實質複製的是其引用,當引用指向的值改變時也會跟著變化。
「深複製」
:複製變數值,對於引用資料,則遞迴至基本型別後,再複製。深複製後的物件
「與原來的物件完全隔離」
,互不影響,對一個物件的修改並不會影響另一個物件。
「深複製和淺複製是針對複雜資料型別來說的,淺複製只複製一層,而深複製是層層複製。」
三、垃圾回收機制(GC)
垃圾回收(Garbage Collection,縮寫為GC)是一種自動的儲存器管理機制。當某個程式佔用的一部分記憶體空間不再被這個程式訪問時,這個程式會藉助垃圾回收演算法向作業系統歸還這部分記憶體空間。垃圾回收器可以減輕程式設計師的負擔,也減少程式中的錯誤。垃圾回收最早起源於LISP語言。目前許多語言如Smalltalk、Java、C#和D語言都支援垃圾回收器,我們熟知的 JavaScript 具有自動垃圾回收機制。
「在 JavaScript 中,原始型別的資料被分配到棧空間中,引用型別的資料會被分配到堆空間中。」
1。 棧空間中的垃圾回收
當函式 showName 呼叫完成後,透過下移 ESP(Extended Stack Pointer)指標,來銷燬 showName 函式,之後呼叫其他函式時,將覆蓋掉舊記憶體,存放另一個函式的執行上下文,實現垃圾回收。圖片來自《瀏覽器工作原理與實踐》
2。 堆空間中的垃圾回收
堆中資料垃圾回收策略的基礎是:
「代際假說」
(The Generational Hypothesis)。即:
大部分物件在記憶體中存在時間極短,很多物件很快就不可訪問。
不死的物件將活得更久。
這兩個特點不僅僅適用於 JavaScript,同樣適用於大多數的動態語言,如 Java、Python 等。V8 引擎將堆空間分為
「新生代」
(存放生存
「時間短」
的物件)和
「老生代」
(存放生存
「時間長」
的物件)兩個區域,並使用不同的垃圾回收器。
副垃圾回收器,主要負責新生代的垃圾回收。
主垃圾回收器,主要負責老生代的垃圾回收。
不管是哪種垃圾回收器,都使用相同垃圾回收流程:
「標記活動物件和非活動物件,回收非活動物件的記憶體,最後記憶體整理。」
**
1。1 副垃圾回收器
使用 Scavenge 演算法處理,將新生代空間對半分為兩個區域,一個物件區域,一個空閒區域。圖片來自《瀏覽器工作原理與實踐》
執行流程:
新物件存在在
「物件區域」
,當物件區域將要寫滿時,執行一次垃圾回收;
垃圾回收過程中,首先對物件區域中的垃圾做標記,然後副垃圾回收器將存活的物件複製並有序排列到空閒區域,相當於完成記憶體整理。
複製完成後,將物件區域和空閒區域翻轉,完成垃圾回收操作,這也讓新生代中兩塊區域無限重複使用。
當然,這也存在一些問題:若複製操作的資料較大則影響清理效率。JavaScript 引擎的解決方式是:將新生代區域設定得比較小,並採用物件晉升策略(經過兩次回收仍存活的物件,會被移動到老生區),避免因為新生代區域較小引起存活物件裝滿整個區域的問題。
1。2 主垃圾回收器
分為:
「標記 - 清除(Mark-Sweep)演算法」
,和
「標記 - 整理(Mark-Compact)演算法」
。
「a)標記 - 清除(Mark-Sweep)演算法」
「過程:」
標記過程:從一組根元素開始遍歷整個元素,能到達的元素為活動物件,反之為垃圾資料;
清除過程:清理被標記的資料,併產生大量碎片記憶體。(缺點:導致大物件無法分配到足夠的連續記憶體)
圖片來自《瀏覽器工作原理與實踐》
「b)標記 - 整理(Mark-Compact)演算法」
「過程:」
標記過程:從一組根元素開始遍歷整個元素,能到達的元素為活動物件,反之為垃圾資料;
整理過程:將所有存活的物件,向一段移動,然後清除端邊界以外的內容。
圖片來自《瀏覽器工作原理與實踐》
3。 拓展閱讀
1。《圖解Java 垃圾回收機制》2。《MDN 記憶體管理》
四、物件方法和 this
1。 物件方法
具體介紹可閱讀 《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
2。 this
2。1 this 簡介
當物件方法需要使用物件中的屬性,可以使用 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 程式碼便正常執行。
2。2 this 取值
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
2。3 箭頭函式沒有自己的 this
箭頭函式比較特別,沒有自己的 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
2。4 call / apply / bind
詳細可以閱讀《js基礎-關於call,apply,bind的一切》 。當我們想把 this 值繫結到另一個環境中,就可以使用 call / apply / bind 方法實現:
var user = { name: ‘leo’ };
var name = ‘pingan’;
functionfun(){
returnconsole。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 運算子
1。 建構函式
建構函式的作用在於
「實現可重用的物件建立程式碼」
。通常,對於建構函式有兩個約定:
命名時首字母大寫;
只能使用 new 運算子執行。
「new 運算子」
建立一個使用者定義的物件型別的例項或具有建構函式的內建物件的例項。語法如下:
newconstructor[([arguments])]
引數如下:
constructor一個指定物件例項的型別的類或函式。
arguments一個用於被 constructor 呼叫的引數列表。
2。 簡單示例
舉個簡單示例:
functionUser (name){
this。name = name;
this。isAdmin = false;
}
const leo = new User(‘leo’);
console。log(leo。name, leo。isAdmin); // “leo” false
3。 new 運算子操作過程
當一個函式被使用 new 運算子執行時,它按照以下步驟:
一個新的空物件被建立並分配給 this。
函式體執行。通常它會修改 this,為其新增新的屬性。
返回 this 的值。
以前面 User 方法為例:
functionUser(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 運算子執行。
4。 建構函式中的方法
在建構函式中,也可以將方法繫結到 this 上:
functionUser (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。 背景介紹
在實際開發中,常常出現下面幾種報錯情況:
// 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
這種寫法的缺點就是
「太麻煩了」
。
2。 可選鏈介紹
可選鏈 ?。 是一種
「訪問巢狀物件屬性的防錯誤方法」
。即使中間的屬性不存在,也不會出現錯誤。如果可選鏈 ?。 前面部分是 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
3。 使用注意
可選鏈雖然好用,但需要注意以下幾點:
「不能過度使用可選鏈」
;
我們應該只將 ?。 使用在一些屬性或方法可以不存在的地方,以上面示例程式碼為例:
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];
4。 其他情況:?。() 和 ?。[]
需要說明的是 ?。 是一個特殊的語法結構,而不是一個運算子,它還可以與其 () 和 [] 一起使用:
4。1 可選鏈與函式呼叫 ?。()
?。() 用於呼叫一個可能不存在的函式,比如:
let user1 = {
admin() {
alert(“I am admin”);
}
}
let user2 = {};
user1。admin?。(); // I am admin
user2。admin?。();
?。() 會檢查它左邊的部分:如果 admin 函式存在,那麼就呼叫執行它(對於 user1)。否則(對於 user2)運算停止,沒有錯誤。
4。2 可選鏈和表示式 ?。[]
?。[] 允許從一個可能不存在的物件上安全地讀取屬性。
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
5。 可選鏈 ?。 語法總結
可選鏈 ?。 語法有三種形式:
obj?。prop —— 如果 obj 存在則返回 obj。prop,否則返回 undefined。
obj?。[prop] —— 如果 obj 存在則返回 obj[prop],否則返回 undefined。
obj?。method() —— 如果 obj 存在則呼叫 obj。method(),否則返回 undefined。
正如我們所看到的,這些語法形式用起來都很簡單直接。?。 檢查左邊部分是否為 null/undefined,如果不是則繼續運算。?。 鏈使我們能夠安全地訪問巢狀屬性。
七、Symbol
規範規定,JavaScript 中物件的屬性只能為
「字串型別」
或者
「Symbol型別」
,畢竟我們也只見過這兩種型別。
1。 概念介紹
ES6引入Symbol作為一種新的
「原始資料型別」
,表示
「獨一無二」
的值,主要是為了
「防止屬性名衝突」
。ES6之後,JavaScript一共有其中資料型別:Symbol、undefined、null、Boolean、String、Number、Object。
「簡單使用」
:
let leo = Symbol();
typeof leo; // “symbol”
Symbol 支援傳入引數作為 Symbol 名,方便程式碼除錯:**
let leo = Symbol(“leo”);
2。 注意事項**
Symbol函式不能用new,會報錯。
由於Symbol是一個原始型別,不是物件,所以不能新增屬性,它是類似於字串的資料型別。
let leo = newSymbol()
// 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] );
3。 字面量中使用 Symbol 作為屬性名
在物件字面量中使用 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’)
}
4。 應用:消除魔術字串
「魔術字串」
:指程式碼中多次出現,強耦合的字串或數值,應該避免,而使用含義清晰的變數代替。
functionfun(name){
if(name == ‘leo’) {
console。log(‘hello’);
}
}
fun(‘leo’); // ‘hello’ 為魔術字串
常使用變數,消除魔術字串:
let obj = {
name: ‘leo’
};
functionfun(name){
if(name == obj。name){
console。log(‘hello’);
}
}
fun(obj。name); // ‘hello’
使用Symbol消除強耦合,使得不需關係具體的值:
let obj = {
name: Symbol()
};
functionfun (name){
if(name == obj。name){
console。log(‘hello’);
}
}
fun(obj。name); // ‘hello’
5。 屬性名遍歷
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 ofObject。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值作為名稱的屬性不被常規方法遍歷獲取,因此常用於定義物件的一些非私有,且內部使用的方法。
6。 Symbol。for()、Symbol。keyFor()
6。1 Symbol。for()
「用於重複使用一個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
6。2 Symbol。keyFor()
「用於返回一個已使用的Symbol型別的key」
:
let leo = Symbol。for(‘leo’);
Symbol。keyFor(leo); // ‘leo’
let leo = Symbol(‘leo’);
Symbol。keyFor(leo); // undefined
7。 內建的Symbol值
ES6提供11個內建的Symbol值,指向語言內部使用的方法:
7。1 Symbol。hasInstance
當其他物件使用instanceof運算子,判斷是否為該物件的例項時,會呼叫這個方法。比如,foo instanceof Foo在語言內部,實際呼叫的是Foo[Symbol。hasInstance](foo)。
classP{
[Symbol。hasInstance](a){
return a instanceofArray;
}
}
[1, 2, 3] instanceofnew P(); // true
P是一個類,new P()會返回一個例項,該例項的Symbol。hasInstance方法,會在進行instanceof運算時自動呼叫,判斷左側的運運算元是否為Array的例項。
7。2 Symbol。isConcatSpreadable
值為布林值,表示該物件用於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’]
7。3 Symbol。species
指向一個建構函式,在建立衍生物件時會使用,使用時需要用get取值器。
classPextendsArray{
staticget [Symbol。species](){
returnthis;
}
}
解決下面問題:
// 問題: b應該是 Array 的例項,實際上是 P 的例項
classPextendsArray{}
let a = new P(1,2,3);
let b = a。map(x => x);
b instanceofArray; // true
b instanceof P; // true
// 解決: 透過使用 Symbol。species
classPextendsArray{
staticget [Symbol。species]() { returnArray; }
}
let a = new P();
let b = a。map(x => x);
b instanceof P; // false
b instanceofArray; // true
7。4 Symbol。match
當執行str。match(myObject),傳入的屬性存在時會呼叫,並返回該方法的返回值。
classP{
[Symbol。match](string){
return‘hello world’。indexOf(string);
}
}
‘h’。match(new P()); // 0
7。5 Symbol。replace
當該物件被String。prototype。replace方法呼叫時,會返回該方法的返回值。
let a = {};
a[Symbol。replace] = (。。。s) =>console。log(s);
‘Hello’。replace(a , ‘World’) // [“Hello”, “World”]
7。6 Symbol。hasInstance
當該物件被String。prototype。search方法呼叫時,會返回該方法的返回值。
classP{
constructor(val) {
this。val = val;
}
[Symbol。search](s){
return s。indexOf(this。val);
}
}
‘hileo’。search(new P(‘leo’)); // 2
7。7 Symbol。split
當該物件被String。prototype。split方法呼叫時,會返回該方法的返回值。
// 重新定義了字串物件的split方法的行為
classP{
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”
7。8 Symbol。iterator
物件進行for。。。of迴圈時,會呼叫Symbol。iterator方法,返回該物件的預設遍歷器。
classP{
*[Symbol。interator]() {
let i = 0;
while(this[i] !== undefined ) {
yieldthis[i];
++i;
}
}
}
let a = new P();
a[0] = 1;
a[1] = 2;
for (let k of a){
console。log(k);
}
7。9。Symbol。toPrimitive
該物件被轉為原始型別的值時,會呼叫這個方法,返回該物件對應的原始型別值。呼叫時,需要接收一個字串引數,表示當前運算模式,運算模式有:
Number : 此時需要轉換成數值
String : 此時需要轉換成字串
Default : 此時可以轉換成數值或字串
let obj = {
[Symbol。toPrimitive](hint) {
switch (hint) {
case‘number’:
return123;
case‘string’:
return‘str’;
case‘default’:
return‘default’;
default:
thrownewError();
}
}
};
2 * obj // 246
3 + obj // ‘3default’
obj == ‘default’// true
String(obj) // ‘str’
7。10 Symbol。toStringTag
在該物件上面呼叫Object。prototype。toString方法時,如果這個屬性存在,它的返回值會出現在toString方法返回的字串之中,表示物件的型別。也就是說,這個屬性可以用來定製[object Object]或[object Array]中object後面的那個字串。
// 例一
({[Symbol。toStringTag]: ‘Foo’}。toString())
// “[object Foo]”
// 例二
classCollection{
get [Symbol。toStringTag]() {
return‘xxx’;
}
}
let x = new Collection();
Object。prototype。toString。call(x) // “[object xxx]”
7。11 Symbol。unscopables
該物件指定了使用with關鍵字時,哪些屬性會被with環境排除。
// 沒有 unscopables 時
classMyClass{
foo() { return1; }
}
var foo = function () { return2; };
with (MyClass。prototype) {
foo(); // 1
}
// 有 unscopables 時
classMyClass{
foo() { return1; }
get [Symbol。unscopables]() {
return { foo: true };
}
}
var foo = function () { return2; };
with (MyClass。prototype) {
foo(); // 2
}
上面程式碼透過指定Symbol。unscopables屬性,使得with語法塊不會在當前作用域尋找foo屬性,即foo將指向外層作用域的變數。
八、原始值轉換
前面複習到字串、數值、布林值等的轉換,但是沒有講到物件的轉換規則,這部分就一起看看:。需要記住幾個規則:
所有物件在布林上下文中都為 true ,並且不存在轉換為布林值的操作,只有字串和數值轉換有。
數值轉換髮生在物件相減或應用數學函式時。如 Date 物件可以相減,如 date1 - date2 結果為兩個時間的差值。
在字串轉換,通常出現在如 alert(obj) 這種形式。
當然我們可以使用特殊的物件方法,對字串和數值轉換進行微調。下面介紹三個型別(hint)轉換情況:
1。 object to string
物件到字串的轉換,當我們對期望一個字串的物件執行操作時,如 “alert”:
// 輸出
alert(obj);
// 將物件作為屬性鍵
anotherObj[obj] = 123;
2。 object to number
物件到數字的轉換,例如當我們進行數學運算時:
// 顯式轉換
let num = Number(obj);
// 數學運算(除了二進位制加法)
let n = +obj; // 一元加法
let delta = date1 - date2;
// 小於/大於的比較
let greater = user1 > user2;
3。 object to default
少數情況下,
「當運算子“不確定”期望值型別時」
。例如,二進位制加法 + 可用於字串(連線),也可以用於數字(相加),所以字串和數字這兩種型別都可以。因此,當二元加法得到物件型別的引數時,它將依據 “default” 來對其進行轉換。此外,如果物件被用於與字串、數字或 symbol 進行 == 比較,這時到底應該進行哪種轉換也不是很明確,因此使用 “default” 。
// 二元加法使用預設 hint
let total = obj1 + obj2;
// obj == number 使用預設 hint
if (user == 1) { 。。。 };
4。 型別轉換演算法
「為了進行轉換,JavaScript 嘗試查詢並呼叫三個物件方法:」
呼叫 obj[Symbol。toPrimitive](hint) —— 帶有 symbol 鍵 Symbol。toPrimitive(系統 symbol)的方法,如果這個方法存在的話,
否則,如果 hint 是 “string” —— 嘗試 obj。toString() 和 obj。valueOf(),無論哪個存在。
否則,如果 hint 是 “number” 或 “default” —— 嘗試 obj。valueOf() 和 obj。toString(),無論哪個存在。
5。 Symbol。toPrimitive
詳細介紹可閱讀《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
6。 toString/valueOf
toString / valueOf 是兩個比較早期的實現轉換的方法。當沒有 Symbol。toPrimitive ,那麼 JavaScript 將嘗試找到它們,並且按照下面的順序進行嘗試:
對於 “string” hint,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() {
returnthis。money;
}
};
alert(user); // 控制檯:hint: string 彈框:{name: “John”}
alert(+user); // 控制檯:hint: number 彈框:9999
alert(user + 1); // 控制檯:hint: default 彈框:10000
總結
本文作為《初中級前端 JavaScript 自測清單》第二部分,介紹的內容以 JavaScript 物件為主,其中有讓我眼前一亮的知識點,如 Symbol。toPrimitive 方法。我也希望這個清單能幫助大家自測自己的 JavaScript 水平並查缺補漏,溫故知新。
推薦文章
- 四川這個尚未開發的秘境,當地人說可媲美九寨溝,門票還免費
那麼其實在黃龍、九寨溝之間存在一個荒無人煙的地方,這裡方圓五十里都尚未被開放,當然這是一條不會被旅行社所推薦的旅遊路線,畢竟危險係數比較高,而且沒有配套的各種措施,比較適合那些愛冒險、闖蕩的朋友們...
- 一種湯汁都被吸收完的紅燒基圍蝦,覺得好吃的請舉手
步驟4:用鍋底剩餘的油,將姜、蒜、小米椒爆香,接著放入青椒和紅椒末,大火翻炒幾下改中火,倒入基圍蝦,加鹽炒勻,再加入一勺生抽,再將蔥段放入,大火爆炒一下,再撒上一點熟芝麻,這樣就可以出鍋了...
- 《前赤壁賦》的由來——蘇軾與佛印的性格結緣!
我們之所以能在《前赤壁賦》的明月清風中撫平塵世的傷痛,讓自己的心靈遺世獨立,羽化登仙,正是因為文中超然和諧的意境,融匯了蘇軾跌宕坎坷的人生體驗、脫俗自由的佛道思想、超然淡遠的美學追求,以及韻散結合的語言藝術...