1.1w字 | 初中級前端 JavaScript 自測清單 - 2

前言

《初中級前端 JavaScript 自測清單 - 1》部分中,和你們簡單過了一遍 JavaScript 基礎知識,沒看過的朋友能夠回顧一下😁javascript

本系列文章是我在咱們團隊內部的「現代 JavaScript 突擊隊」,第一期學習內容爲《現代 JavaScript 教程》系列的第二部分輸出內容,但願這份自測清單,可以幫助你們鞏固知識,溫故知新。前端

本部份內容,以 JavaScript 對象爲主,大體包括如下內容:
初中級前端 JavaScript 自測清單 2.pngjava

1、對象

JavaScript 有八種數據額類型,有七種原始類型,它們值只包含一種類型(字符串,數字或其餘),而對象是用來保存鍵值對和更復雜實體。
咱們能夠經過使用帶有可選屬性列表的花括號 **{...}** 來建立對象,一個屬性就是一個鍵值對 {"key" : "value"} ,其中鍵( key )是一個字符串(或稱屬性名),值( value )能夠是任何類型。程序員

1. 建立對象

咱們可使用 2 種方式來建立一個新對象:面試

// 1. 經過「構造函數」建立
let user = new Object();

// 2. 經過「字面量」建立
let user = {};

2. 對象文本和屬性

建立對象時,能夠初始化對象的一些屬性:算法

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

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 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;
}

4. Object.getOwnPropertyNames(Obj)

該方法返回一個數組,它包含了對象 Obj 全部擁有的屬性(不管是否可枚舉)的名稱。

let user = { name: "leo", age: 18};
Object.getOwnPropertyNames(user);
// ["name", "age"]

2、對象拷貝

參考文章《搞不懂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拷貝;
  • undefinednull沒法轉成對象,它們不能做爲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() 方法返回一個新的數組對象,這一對象是一個由 beginend 決定的原數組的淺拷貝(包括 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 手寫淺拷貝

實現原理:新的對象複製已有對象中非對象屬性的值和對象屬性的引用,也就是說對象屬性並不複製到內存。

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

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() 使用注意:

  • 拷貝的對象的值中若是有函數, undefinedsymbol 則通過 JSON.stringify() `序列化後的JSON字符串中這個鍵值對會消失;
  • 沒法拷貝不可枚舉的屬性,沒法拷貝對象的原型鏈;
  • 拷貝 Date 引用類型會變成字符串;
  • 拷貝 RegExp 引用類型會變成空對象;
  • 對象中含有 NaNInfinity-Infinity ,則序列化的結果會變成 null
  • 沒法拷貝對象的循環應用(即 obj[key] = obj )。

2. 第三方庫

4.3 手寫深拷貝

核心思想是遞歸,遍歷對象、數組直到裏邊都是基本數據類型,而後再去複製,就是深度拷貝。 實現代碼:

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;
}

該方法缺陷: 遇到循環引用,會陷入一個循環的遞歸過程,從而致使爆棧。
其餘寫法,能夠閱讀《如何寫出一個驚豔面試官的深拷貝?》

5. 小結

淺拷貝:將對象的每一個屬性進行依次複製,可是當對象的屬性值是引用類型時,實質複製的是其引用,當引用指向的值改變時也會跟着變化。

深拷貝:複製變量值,對於引用數據,則遞歸至基本類型後,再複製。深拷貝後的對象與原來的對象徹底隔離,互不影響,對一個對象的修改並不會影響另外一個對象。

深拷貝和淺拷貝是針對複雜數據類型來講的,淺拷貝只拷貝一層,而深拷貝是層層拷貝。
深拷貝和淺拷貝

3、垃圾回收機制(GC)

垃圾回收(Garbage Collection,縮寫爲GC))是一種自動的存儲器管理機制。當某個程序佔用的一部份內存空間再也不被這個程序訪問時,這個程序會藉助垃圾回收算法向操做系統歸還這部份內存空間。垃圾回收器能夠減輕程序員的負擔,也減小程序中的錯誤。垃圾回收最先起源於LISP語言。
目前許多語言如Smalltalk、Java、C#和D語言都支持垃圾回收器,咱們熟知的 JavaScript 具備自動垃圾回收機制。

在 JavaScript 中,原始類型的數據被分配到棧空間中,引用類型的數據會被分配到堆空間中。

1. 棧空間中的垃圾回收

當函數 showName 調用完成後,經過下移 ESP(Extended Stack Pointer)指針,來銷燬 showName 函數,以後調用其餘函數時,將覆蓋掉舊內存,存放另外一個函數的執行上下文,實現垃圾回收。

圖片來自《瀏覽器工做原理與實踐》

2. 堆空間中的垃圾回收

堆中數據垃圾回收策略的基礎是:代際假說(The Generational Hypothesis)。即:

  1. 大部分對象在內存中存在時間極短,不少對象很快就不可訪問。
  2. 不死的對象將活得更久。

這兩個特色不只僅適用於 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 內存管理》

4、對象方法和 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() 被調用,則 thisfun 函數調用期間是 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';
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, ...)

5、構造函數和 new 運算符

1. 構造函數

構造函數的做用在於 實現可重用的對象建立代碼
一般,對於構造函數有兩個約定:

  • 命名時首字母大寫;
  • 只能使用 new 運算符執行。

new 運算符建立一個用戶定義的對象類型的實例或具備構造函數的內置對象的實例。
語法以下:

new constructor[([arguments])]

參數以下:

  • constructor一個指定對象實例的類型的類或函數。
  • arguments一個用於被 constructor 調用的參數列表。

2. 簡單示例

舉個簡單示例:

function User (name){
    this.name = name;
  this.isAdmin = false; 
}
const leo = new User('leo');
console.log(leo.name, leo.isAdmin); // "leo" false

3. new 運算符操做過程

當一個函數被使用 new 運算符執行時,它按照如下步驟:

  1. 一個新的空對象被建立並分配給 this
  2. 函數體執行。一般它會修改 this,爲其添加新的屬性。
  3. 返回 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') 時,發生如下事情:

  1. 一個繼承自 User.prototype 的新對象被建立;
  2. 使用指定參數調用構造函數 User ,並將 this 綁定到新建立的對象;
  3. 由構造函數返回的對象就是 new 表達式的結果。若是構造函數沒有顯式返回一個對象,則使用步驟1建立的對象。

須要注意

  1. 通常狀況下,構造函數不返回值,可是開發者能夠選擇主動返回對象,來覆蓋正常的對象建立步驟;
  2. new User 等同於 new User() ,只是沒有指定參數列表,即 User 不帶參數的狀況;
let user = new User; // <-- 沒有參數
// 等同於
let user = new User();
  1. 任何函數均可以做爲構造器,即均可以使用 new 運算符運行。

4. 構造函數中的方法

在構造函數中,也能夠將方法綁定到 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"

6、可選鏈 "?."

詳細介紹能夠查看 《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. 使用注意

可選鏈雖然好用,但須要注意如下幾點:

  1. 不能過分使用可選鏈

咱們應該只將 ?. 使用在一些屬性或方法能夠不存在的地方,以上面示例代碼爲例:

const leo = {};
console.log(leo.name?.toString());

這樣寫會更好,由於 leo 對象是必須存在,而 name 屬性則可能不存在。

  1. 可選鏈 ?. 以前的變量必須已聲明

在可選鏈 ?. 以前的變量必須使用 let/const/var 聲明,不然會報錯:

leo?.name;
// Uncaught ReferenceError: leo is not defined
  1. 可選鏈不能用於賦值
let object = {};
object?.property = 1; 
// Uncaught SyntaxError: Invalid left-hand side in assignment
  1. 可選鏈訪問數組元素的方法
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. 可選鏈 ?. 語法總結

可選鏈 ?. 語法有三種形式:

  1. obj?.prop —— 若是 obj 存在則返回 obj.prop,不然返回 undefined
  2. obj?.[prop] —— 若是 obj 存在則返回 obj[prop],不然返回 undefined
  3. obj?.method() —— 若是 obj 存在則調用 obj.method(),不然返回 undefined

正如咱們所看到的,這些語法形式用起來都很簡單直接。?. 檢查左邊部分是否爲 null/undefined,若是不是則繼續運算。
?. 鏈使咱們可以安全地訪問嵌套屬性。

8、Symbol

規範規定,JavaScript 中對象的屬性只能爲 字符串類型 或者 Symbol類型 ,畢竟咱們也只見過這兩種類型。

1. 概念介紹

ES6引入Symbol做爲一種新的原始數據類型,表示獨一無二的值,主要是爲了防止屬性名衝突
ES6以後,JavaScript一共有其中數據類型:SymbolundefinednullBooleanStringNumberObject
簡單使用

let leo = Symbol();
typeof leo; // "symbol"

Symbol 支持傳入參數做爲 Symbol 名,方便代碼調試:
**

let leo = Symbol("leo");

2. 注意事項**

  • 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] );

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. 應用:消除魔術字符串

魔術字符串:指代碼中屢次出現,強耦合的字符串或數值,應該避免,而使用含義清晰的變量代替。

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'

5. 屬性名遍歷

Symbol做爲屬性名遍歷,不出如今for...infor...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值做爲名稱的屬性不被常規方法遍歷獲取,所以經常使用於定義對象的一些非私有,且內部使用的方法。

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)

class P {
    [Symbol.hasInstance](a){
        return a instanceof Array;
    }
}
[1, 2, 3] instanceof new 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取值器。

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

7.4 Symbol.match

當執行str.match(myObject),傳入的屬性存在時會調用,並返回該方法的返回值。

class P {
    [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方法調用時,會返回該方法的返回值。

class P {
    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方法的行爲
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"

7.8 Symbol.iterator

對象進行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);
}

7.9.Symbol.toPrimitive

該對象被轉爲原始類型的值時,會調用這個方法,返回該對象對應的原始類型值。調用時,須要接收一個字符串參數,表示當前運算模式,運算模式有:

  • Number : 此時須要轉換成數值
  • String : 此時須要轉換成字符串
  • Default : 此時能夠轉換成數值或字符串
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'

7.10 Symbol.toStringTag

在該對象上面調用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]"

7.11 Symbol.unscopables

該對象指定了使用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將指向外層做用域的變量。

9、原始值轉換

前面複習到字符串、數值、布爾值等的轉換,可是沒有講到對象的轉換規則,這部分就一塊兒看看:。
須要記住幾個規則:

  1. 全部對象在布爾上下文中都爲 true ,而且不存在轉換爲布爾值的操做,只有字符串和數值轉換有。
  2. 數值轉換髮生在對象相減或應用數學函數時。如 Date 對象能夠相減,如 date1 - date2 結果爲兩個時間的差值。
  3. 在字符串轉換,一般出如今如 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 嘗試查找並調用三個對象方法:

  1. 調用 obj[Symbol.toPrimitive](hint) —— 帶有 symbol 鍵 Symbol.toPrimitive(系統 symbol)的方法,若是這個方法存在的話,
  2. 不然,若是 hint 是 "string" —— 嘗試 obj.toString()obj.valueOf(),不管哪一個存在。
  3. 不然,若是 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

這兩個方法必須返回一個原始值。若是 toStringvalueOf 返回了一個對象,那麼返回值會被忽略。默認狀況下,普通對象具備 toStringvalueOf 方法:

  • 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 水平並查缺補漏,溫故知新。

相關文章
相關標籤/搜索