相關(guān)知識(shí))
一、什么是函數(shù)函數(shù)是一個(gè)“可執(zhí)行的代碼塊”但在 JavaScript 中它本質(zhì)上是一種特殊的對(duì)象Object它擁有普通對(duì)象沒有的“內(nèi)部屬性”[[Call]]這就是它能被執(zhí)行的原因。特殊之處1.可執(zhí)行可以用 () 調(diào)用因?yàn)橛?[[Call]] 內(nèi)部槽。2.可掛載屬性因?yàn)樗菍?duì)象所以你可以給它掛屬性fn.myName ‘test’。3.可被 new只要函數(shù)有 [[Construct]] 內(nèi)部槽就能當(dāng)構(gòu)造函數(shù)使用。1.1 理解Object.prototype**Object.prototype 是所有 JavaScript 對(duì)象的根原型。**它是個(gè)普通對(duì)象里面裝著 toString、hasOwnProperty 等所有對(duì)象都能用的通用方法。它的proto指向 null是原型鏈的終點(diǎn)指向null是為了終止整個(gè)鏈條避免無(wú)限循環(huán)。因此Object.prototype.proto為null無(wú)論你是普通對(duì)象、數(shù)組、函數(shù)還是 Date、RegExp最終都會(huì)通過proto鏈找到 Object.prototype從而繼承這些通用方法。1.2 關(guān)于prototype 、proto、constructor 的表述1所有函數(shù)除了箭頭函數(shù)都自有一個(gè) prototype 對(duì)象。這個(gè) prototype 對(duì)象里有一個(gè)自有的 constructor 屬性以及一個(gè)從 Object.prototype 繼承來(lái)的proto訪問器屬性。2函數(shù)本身作為對(duì)象還自有一個(gè)proto屬性指向 Function.prototype并且通過這個(gè)proto繼承了一個(gè) constructor 屬性指向 Function??偨Y(jié)1prototype函數(shù)才有箭頭函數(shù)除外是一個(gè)對(duì)象存放共享數(shù)據(jù)即方法和屬性。2proto所有對(duì)象都有指向自己的原型形成原型鏈接收prototype共享的數(shù)據(jù)。舉例假設(shè)有類Ab是A的實(shí)例對(duì)象const b new A()那么 b.proto A.prototype3constructorprototype 對(duì)象上的屬性指向構(gòu)造函數(shù)本身。這三個(gè)屬性的終極作用1.prototype 是為了讓函數(shù)隨時(shí)具備成為構(gòu)造函數(shù)的能力是給實(shí)例提供共享方法的容器。2.proto是為了讓函數(shù)作為對(duì)象時(shí)能繼承通用方法如 call、apply。3.constructor 是為了讓原型鏈保持雙向完整支持 instanceof 等操作。二、函數(shù)分為哪幾類按照定義方式、調(diào)用方式和內(nèi)部槽[[Call]] / [[Construct]]我們分為以下五類2.1 第一類普通函數(shù)能否new可以能否 this 綁定動(dòng)態(tài)綁定典型場(chǎng)景常規(guī)業(yè)務(wù)邏輯、構(gòu)造函數(shù)它有 prototype 和proto并通過proto鏈從 Function.prototype 上繼承了constructorfunctionsum(a,b){returnab;}console.log(sum(1,2));// 3console.log(sum.prototype);// { constructor: sum } 可作為構(gòu)造函數(shù)// constructorconsole.log(sum.prototype.constructorsum);// true它們是同一個(gè)對(duì)象,sum.prototype.constructor 指向的是 sum 函數(shù)本身這個(gè)對(duì)象console.log(sum.prototype.constructor.name);// sum這才是函數(shù)名字符串/** sum.prototype.__proto__ 的值是 Object.prototype因?yàn)?sum.prototype 是一個(gè)普通對(duì)象它的原型指向 Object.prototype */console.log(sum.prototype.__proto__)functiongreet(){console.log(Hello);}// 1. 有 prototype作為構(gòu)造函數(shù)身份console.log(greet.prototype);// { constructor: greet, __proto__: Object.prototype }// 2. 有 __proto__作為對(duì)象身份console.log(greet.__proto__);// Function.prototype// 3. 沒有自己的 constructorconsole.log(greet.hasOwnProperty(constructor));// false// 4. 但可以訪問到 constructor從原型鏈上找到的console.log(greet.constructor);// Functionconsole.log(greet.constructorFunction);// true// 因?yàn)?greet.__proto__ 指向 Function.prototype而 Function.prototype.constructor Function2.2 第二類箭頭函數(shù)能否new不能無(wú) prototype能否 this 綁定不能繼承外層 this典型場(chǎng)景回調(diào)函數(shù)、setTimeout箭頭函數(shù)沒有 prototype 屬性因?yàn)樗荒鼙?new 調(diào)用但它有proto屬性因?yàn)樗菍?duì)象并且通過proto鏈從 Function.prototype 上繼承了 constructor 屬性。constarrow(){console.log(this);// 繼承外層 this指向全局或外層上下文};arrow();console.log(arrow.prototype);// undefined 不能 new2.3 第三類方法函數(shù)能否new 不能除非是 class 方法能否 this 綁定綁定到所屬對(duì)象典型場(chǎng)景對(duì)象行為、類原型方法constobj{name:JS,show(){// ES6 簡(jiǎn)寫方法console.log(this.name);}};obj.show();// JS2.4 第四類構(gòu)造函數(shù)/類能否new 必須用 new能否 this 綁定綁定到新創(chuàng)建實(shí)例典型場(chǎng)景面向?qū)ο缶幊獭?shí)例化functionDog(name){this.namename;}constdnewDog(旺財(cái));console.log(d.name);// 旺財(cái)// 等價(jià)于 classclassCat{constructor(name){this.namename;}}2.5 第五類生成器函數(shù)能否new 不能next() 執(zhí)行能否 this 綁定 動(dòng)態(tài)綁定典型場(chǎng)景異步流程控制、迭代器function*gen(){yield1;yield2;return3;}constggen();console.log(g.next());// { value: 1, done: false }console.log(g.next());// { value: 2, done: false }console.log(g.next());// { value: 3, done: true }三、JavaScript 原型鏈繼承機(jī)制prototype、proto、constructor這三者共同編織了原型鏈。當(dāng)我們?cè)噲D訪問一個(gè)對(duì)象的屬性時(shí)JavaScript引擎會(huì)首先在對(duì)象自身上查找。如果找不到它就會(huì)沿著該對(duì)象的proto 指針去其原型對(duì)象上查找。如果原型對(duì)象上依然沒有它會(huì)繼續(xù)沿著原型對(duì)象的proto向上查找如此往復(fù)直到原型鏈的頂端-Object.prototype。如果最終仍未找到則返回undefined。這種通過 proto鏈接起來(lái)的、自下而上的查找路徑就是原型鏈繼承的核心機(jī)制1、所有函數(shù)通過proto鏈先繼承 Function.prototype獲得 call、apply、bind而 Function.prototype 的proto又指向 Object.prototype所以函數(shù)最終也繼承了 Object.prototype 的 toString、hasOwnProperty 等通用方法。– 所有函數(shù)都是 Function 的實(shí)例所以 函數(shù).proto Function.prototype。– Function.prototype 本身是一個(gè)對(duì)象所以 Function.prototype.proto Object.prototype因此函數(shù)通過“串聯(lián)繼承”同時(shí)擁有了兩套原型上的方法2、箭頭函數(shù)和普通函數(shù)在繼承鏈上沒有任何區(qū)別。唯一的區(qū)別是箭頭函數(shù)沒有自己的 prototype 屬性因?yàn)樗荒鼙?new 調(diào)用。function Person() {} ┌─────────────────────────────────────────────────────────────────┐ │ Person 函數(shù)對(duì)象它本身 │ │ ├── Person.prototype ──────────────────────────────────────┐ │ │ ├── Person.__proto__ ────→ Function.prototype │ │ │ └── Person.constructor ──→ Function從 Function.prototype │ │ │ 繼承來(lái)的 │ │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Person.prototype 對(duì)象它是 Person 的“模具” │ │ ├── Person.prototype.constructor ──→ Person 函數(shù)本身自有 │ │ │ └── Person.prototype.__proto__ ────→ Object.prototype │ │ └─────────────────────────────────────────────────────────────────┘繼承路徑函數(shù)對(duì)象函數(shù) → Function.prototype → Object.prototype → null實(shí)例對(duì)象實(shí)例 → 構(gòu)造函數(shù).prototype → Object.prototype → null普通對(duì)象對(duì)象 → Object.prototype → null箭頭函數(shù)繼承路徑與普通函數(shù)完全一致。唯一區(qū)別是沒有 prototype 屬性不能作為構(gòu)造函數(shù)。四、原型鏈相關(guān)方法4.1 hasOwnProperty ---- Object.hasOwn(obj, key)最新方案作用檢查屬性是否是對(duì)象自身的非繼承應(yīng)用場(chǎng)景for…in 循環(huán)中過濾原型屬性判斷對(duì)象是否有某個(gè)自有字段functionPerson(){}Person.prototype.name祖先;constpnewPerson();p.age18;console.log(p.hasOwnProperty(age));// true自己的console.log(p.hasOwnProperty(name));// false繼承來(lái)的//在 for...in 循環(huán)中過濾掉繼承的屬性避免遍歷到原型鏈上的方法。for(letkeyinp){if(p.hasOwnProperty(key)){console.log(key);// 只打印 age不會(huì)打印 name}}hasOwnProperty 的致命弱點(diǎn) 如果對(duì)象自身有一個(gè)名為 hasOwnProperty 的屬性obj.hasOwnProperty 就會(huì)被覆蓋導(dǎo)致調(diào)用失敗。因此推薦使用Object.hasOwn(obj, key)最新方案constobj{name:小明,hasOwnProperty:我是冒名頂替的// 覆蓋了原型上的 hasOwnProperty};console.log(Object.hasOwn(obj,name));// true依然正常工作console.log(Object.hasOwn(obj,age));// false4.2 Object.keys()作用獲取對(duì)象自己的屬性名列表應(yīng)用場(chǎng)景遍歷對(duì)象屬性、將對(duì)象轉(zhuǎn)換為數(shù)組、React 中渲染動(dòng)態(tài)表單constobj{a:1,b:2};Object.prototype.hidden我是原型上的;console.log(Object.keys(obj));// [a, b]只返回自己的不包含繼承的// React 中渲染動(dòng)態(tài)表單constformData{name:小明,age:18};returnObject.keys(formData).map(key(Input key{key}label{key}value{formData[key]}/));4.3 Object.getOwnPropertyNames()作用返回對(duì)象自身所有屬性名包括不可枚舉的如 length應(yīng)用場(chǎng)景調(diào)試、深度拷貝、需要獲取 length 等不可枚舉屬性時(shí)constarr[1,2,3];console.log(Object.keys(arr));// [0, 1, 2]不包含 lengthconsole.log(Object.getOwnPropertyNames(arr));// [0, 1, 2, length]包含不可枚舉的 length4.4 Object.getPrototypeOf()作用獲取對(duì)象的原型推薦替代proto應(yīng)用場(chǎng)景獲取原型鏈、實(shí)現(xiàn)繼承判斷、調(diào)試functionPerson(){}constpnewPerson();console.log(Object.getPrototypeOf(p)Person.prototype);// trueconsole.log(Object.getPrototypeOf(p)p.__proto__);// true但推薦用前者4.5 Object.values(obj)作用獲取對(duì)象自己的屬性值列表constobj{a:1,b:2};console.log(Object.values(obj));// [1, 2]4.6 Object.entries(obj)作用獲取 [key, value] 數(shù)組console.log(Object.entries(obj));// [[a, 1], [b, 2]]4.7 in 和 instanceofin檢查屬性是否存在于對(duì)象或其原型鏈上instanceof檢查構(gòu)造函數(shù)的 prototype 是否在對(duì)象的原型鏈上constobj{name:小明};console.log(nameinobj);// true自有console.log(toStringinobj);// true繼承來(lái)的console.log(Object.hasOwn(obj,toString));// false區(qū)分自有/繼承4.8 Object.create(null)Object.create(null) 會(huì)創(chuàng)建一個(gè)完全沒有原型的對(duì)象。它的proto是 null因此它不繼承 Object.prototype 上的任何方法比如 toString、hasOwnProperty、valueOf 等。constpureObjObject.create(null);console.log(pureObj.__proto__);// undefinedconsole.log(pureObj.toString);// undefinedconsole.log(pureObj.hasOwnProperty);// undefined為什么需要這樣一個(gè)“純凈”的對(duì)象因?yàn)槠胀▽?duì)象{}繼承了很多默認(rèn)方法這些方法可能會(huì)在特定場(chǎng)景下造成意想不到的副作用或安全風(fēng)險(xiǎn)。假設(shè)你寫了一個(gè)函數(shù)用來(lái)把用戶輸入的 key 和 value 存儲(chǔ)到一個(gè)對(duì)象里functionsetValue(obj,key,value){obj[key]value;returnobj;}constuserData{};setValue(userData,name,小明);setValue(userData,toString,惡意內(nèi)容);// 用戶把 key 設(shè)為 toStringconsole.log(userData.toString);// 惡意內(nèi)容覆蓋了原型上的 toString// 更危險(xiǎn)的是如果用戶傳入 __proto__會(huì)污染所有對(duì)象的原型如果用戶傳入的 key 是proto或 constructor就可能污染全局原型鏈導(dǎo)致安全漏洞。使用 Object.create(null) 創(chuàng)建的對(duì)象沒有原型因此即使被寫入proto或 toString也不會(huì)影響任何其他對(duì)象因?yàn)樗鼈冎皇亲鳛槠胀ㄦI值對(duì)存在。Object.create(null) 的使用場(chǎng)景場(chǎng)景 1用作字典 / 緩存最常用constcacheObject.create(null);cache[user_123]{name:小明};cache[user_456]{name:小紅};// 安全地檢查 key 是否存在不會(huì)觸發(fā)原型鏈上的意外屬性if(cache[user_123]){console.log(cache[user_123]);// { name: 小明 }}2場(chǎng)景 2存儲(chǔ)用戶輸入的數(shù)據(jù)防止原型污染functionsafeStore(userInput){conststoreObject.create(null);for(letkeyinuserInput){store[key]userInput[key];}returnstore;}constinput{name:小明,__proto__:{admin:true}};constsafeDatasafeStore(input);console.log(safeData.__proto__);// undefined安全性能敏感的映射表無(wú)原型鏈查找開銷普通對(duì)象在訪問屬性時(shí)如果找不到會(huì)沿著原型鏈向上查找。Object.create(null) 沒有原型鏈訪問不存在的屬性會(huì)立即返回 undefined沒有額外的查找開銷。constnormalMap{};constpureMapObject.create(null);console.time(普通對(duì)象);for(leti0;i1000000;i){normalMap[keyi]i;}console.timeEnd(普通對(duì)象);console.time(純凈對(duì)象);for(leti0;i1000000;i){pureMap[keyi]i;}console.timeEnd(純凈對(duì)象);使用 Object.create(null) 的注意事項(xiàng)1·它沒有任何內(nèi)置方法2.它無(wú)法通過 for…in 遍歷原型鏈上的屬性因?yàn)楦緵]有總結(jié)┌─────────────────────────────────────────────────────────────────┐ │ 原型鏈方法全圖 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 查自有屬性不查原型鏈 │ │ ├── Object.hasOwn(obj, key) ? 推薦ES2022 │ │ └── obj.hasOwnProperty(key) ?? 舊寫法可被覆蓋 │ │ │ │ 遍歷自有屬性 │ │ ├── Object.keys(obj) 可枚舉屬性名 │ │ ├── Object.values(obj) 可枚舉屬性值 │ │ ├── Object.entries(obj) [key, value] 數(shù)組 │ │ └── Object.getOwnPropertyNames(obj) 含不可枚舉屬性 │ │ │ │ 操作原型鏈 │ │ ├── Object.getPrototypeOf(obj) 獲取原型 │ │ ├── Object.create(proto) 創(chuàng)建對(duì)象并指定原型 │ │ └── Object.setPrototypeOf(obj, proto) ?? 幾乎不用 │ │ │ │ 類型判斷 │ │ ├── obj instanceof Constructor 檢查原型鏈 │ │ └── A.prototype.isPrototypeOf(B) 更直觀的原型鏈檢查 │ │ │ │ 檢查是否存在含原型鏈 │ │ └── key in obj 不區(qū)分自有/繼承 │ └─────────────────────────────────────────────────────────────────┘// 日常開發(fā)中最常用的 6 個(gè)操作constobj{name:小明,age:18};// 1. 判斷是否自有Object.hasOwn(obj,name);// true// 2. 獲取所有鍵Object.keys(obj);// [name, age]// 3. 獲取所有值Object.values(obj);// [小明, 18]// 4. 遍歷鍵值對(duì)Object.entries(obj).forEach(([key,value]){console.log(key,value);});// 5. 獲取原型Object.getPrototypeOf(obj);// Object.prototype// 6. 判斷類型objinstanceofObject;// true