# JavaScript基礎
點擊關注本[公眾號](http://www.hmoore.net/book/dsh225/javascript_vue_css/edit#_118)獲取文檔最新更新,并可以領取配套于本指南的《**前端面試手冊**》以及**最標準的簡歷模板**.
終于到了大家最擅長的JavaScript部分,相比于HTML和CSS筆者寫起JavaScript要順手很多,雖然前端有三劍客的說法,但是實際應用中基本就是JavaScript為絕對主導,尤其是在工程化的今天。
所以JavaScript才是前端基礎面試中的重中之重,在這部分我們會加入一個新的部分就是原理性的解釋。
比如,我們會有一個面試問題『解釋下變量提升?』,在本章下我們會有一個簡短的解釋,但是不會解釋原理性的東西,因為『簡短的解釋』是給面試官聽的,『原理性的』是給自己解釋的,原理性的解釋會在相關問題下連接到其他各個原理性詳解的章節。
再說一下為什么會有『原理詳解』這一part,本項目并不僅想作為面試季幫助大家突擊的一個清單,更想做的是幫助大家梳理前端的各個知識點,并把知識點講透徹,這才是真正對每個開發者有成長的事情。
此外,如果不懂原理,很容易被較真的面試官追問,一下就原形畢露了,所以如果你不懂原理,建議閱讀原理部分,如果你已經懂了,可以看簡答部分作為梳理即可。
> 我們約定,每個問題后我們標記『?』的為高頻面試題
## 本章索引
[TOC]
## 解釋下變量提升??
JavaScript引擎的工作方式是,先解析代碼,獲取所有被聲明的變量,然后再一行一行地運行。這造成的結果,就是所有的變量的聲明語句,都會被提升到代碼的頭部,這就叫做變量提升(hoisting)。
~~~
console.log(a) // undefined
var a = 1
function b() {
console.log(a)
}
b() // 1
~~~
上面的代碼實際執行順序是這樣的:
第一步: 引擎將`var a = 1`拆解為`var a = undefined`和`a = 1`,并將`var a = undefined`放到最頂端,`a = 1`還在原來的位置
這樣一來代碼就是這樣:
~~~
var a = undefined
console.log(a) // undefined
a = 1
function b() {
console.log(a)
}
b() // 1
~~~
第二步就是執行,因此js引擎一行一行從上往下執行就造成了當前的結果,這就叫變量提升。
> 原理詳解請移步,[預解釋與變量提升](https://www.cxymsg.com/guide/hoisting.html)
## 一段JavaScript代碼是如何執行的??
> 此部分涉及概念較多,請移步[JavaScript執行機制](https://www.cxymsg.com/guide/mechanism)
## 理解閉包嗎??
這個問題其實在問:
1. 閉包是什么?
2. 閉包有什么作用?
### 閉包是什么
MDN的解釋:閉包是函數和聲明該函數的詞法環境的組合。
按照我的理解就是:閉包 =『函數』和『函數體內可訪問的變量總和』
舉個簡單的例子:
~~~
(function() {
var a = 1;
function add() {
var b = 2
var sum = b + a
console.log(sum); // 3
}
add()
})()
~~~
`add`函數本身,以及其內部可訪問的變量,即`a = 1`,這兩個組合在一起就被稱為閉包,僅此而已。
### 閉包的作用
閉包最大的作用就是隱藏變量,閉包的一大特性就是**內部函數總是可以訪問其所在的外部函數中聲明的參數和變量,即使在其外部函數被返回(壽命終結)了之后**
基于此特性,JavaScript可以實現私有變量、特權變量、儲存變量等
我們就以私有變量舉例,私有變量的實現方法很多,有靠約定的(變量名前加\_),有靠Proxy代理的,也有靠Symbol這種新數據類型的。
但是真正廣泛流行的其實是使用閉包。
~~~
function Person(){
var name = 'cxk';
this.getName = function(){
return name;
}
this.setName = function(value){
name = value;
}
}
const cxk = new Person()
console.log(cxk.getName()) //cxk
cxk.setName('jntm')
console.log(cxk.getName()) //jntm
console.log(name) //name is not defined
~~~
函數體內的`var name = 'cxk'`只有`getName`和`setName`兩個函數可以訪問,外部無法訪問,相對于將變量私有化。
## JavaScript的作用域鏈理解嗎??
JavaScript屬于靜態作用域,即聲明的作用域是根據程序正文在編譯時就確定的,有時也稱為詞法作用域。
其本質是JavaScript在執行過程中會創造可執行上下文,可執行上下文中的詞法環境中含有外部詞法環境的引用,我們可以通過這個引用獲取外部詞法環境的變量、聲明等,這些引用串聯起來一直指向全局的詞法環境,因此形成了作用域鏈。

> 原理詳解請移步[JavaScript執行機制](https://www.cxymsg.com/guide/jsBasic.html#mechanism)
## js的幾種設計模式
### 工廠模式
> **【簡單工廠模式】:**可以理解為**解決多個相似的問題【提示框,只是提示的文字需要修改】**
```
// 創建蘋果類
class Apple {
constructor(){
this.name = 'apple'
}
getColor(){
return 'Red'
}
}
// 創建香蕉類
class Banana {
constructor(name){
this.name = 'banana'
this.count = 10
}
getCount(){
return this.count--
}
}
class Fruits {
constructor(type){
switch(type){
case 'apple':
return new Apple()
case 'banana':
return new Banana()
}
}
}
const apple = new Fruits('apple')
const banana = new Fruits('banana')
```
> **抽象工廠模式】:**將其成員**對象的實列化推遲到子類中**,**子類可以重寫父類接口方法以便創建的時候指定自己的對象類型【各種UI組件,根據你要的類型不同(比如:按鈕,提示框,表格等)】[參考鏈接](https://blog.csdn.net/qq_33732195/article/details/110101808?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522167768315316800225564510%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=167768315316800225564510&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-110101808-null-null.142%5Ev73%5Epc_search_v2,201%5Ev4%5Eadd_ask,239%5Ev2%5Einsert_chatgpt&utm_term=%E6%8A%BD%E8%B1%A1%E5%B7%A5%E5%8E%82&spm=1018.2226.3001.4187)**
### 單例模式
兩個特點:一個類**只有一個實例**,并且**提供可全局訪問點**全局對象是最簡單的單例模式:window
**demo:登錄彈出框只需要實例化一次,就可以反復用了**
```
// 實現單例模式彈窗
var createWindow = (function(){
var div;
return function(){
if(!div) {
div = document.createElement("div");
div.innerHTML = "我是彈窗內容";
div.style.display = 'none';
document.body.appendChild(div);
}
return div;
}
})();
document.getElementById("Id").onclick = function(){
// 點擊后先創建一個div元素
var win = createWindow();
win.style.display = "block";
}
```
### 模塊模式
>**模塊模式的思路是為單體模式添加私有變量和私有方法能夠減少全局變量的使用**
**demo:返回對象的匿名函數。在這個匿名函數內部,先定義了私有變量和函數**
```
var singleMode = (function(){
// 創建私有變量
var privateNum = 112;
// 創建私有函數
function privateFunc(){
// 實現自己的業務邏輯代碼
}
// 返回一個對象包含公有方法和屬性
return {
publicMethod1: publicMethod1,
publicMethod2: publicMethod1
};
})();
```
### 代理模式
>**代理對象可以代替本體被實例化,并使其可以被遠程訪問**
**demo: 虛擬代理實現圖片的預加載**
```
class MyImage {
constructor() {
this.img = new Image()
document.body.appendChild(this.img)
}
setSrc(src) {
this.img.src = src
}
}
class ProxyImage {
constructor() {
this.proxyImage = new Image()
}
setSrc(src) {
let myImageObj = new MyImage()
myImageObj.img.src = 'file://xxx.png' //為本地圖片url
this.proxyImage.src = src
this.proxyImage.onload = function() {
myImageObj.img.src = src
}
}
}
var proxyImage = new ProxyImage()
proxyImage.setSrc('http://xxx.png') //服務器資源url
```
### 緩存代理
>**緩存代理的含義就是對第一次運行時候進行緩存,當再一次運行相同的時候,直接從緩存里面取,這樣做的好處是避免重復一次運算功能,如果運算非常復雜的話,對性能很耗費,那么使用緩存對象可以提高性能;**
**demo:計算值的加法,如果之前已經算過,取緩存,如果沒有算過重新計算。**
### 命令模式
>**有時候需要向某些對象發送請求,但是并不知道請求的接收者是誰,也不知道請求的操作是什么,此時希望用一種松耦合的方式來設計程序代碼;使得請求發送者和請求接受者消除彼此代碼中的耦合關系。**
**demo:幾個按鈕綁定不同的事件,然后bindEvent(el, event);**
### 模板方法模式
>一、模板方法模式:一種只需使用繼承就可以實現的非常簡單的模式。
二、模板方法模式由兩部分組成,第一部分是抽象父類,第二部分是具體的實現子類。
demo: 比如泡茶、沖咖啡的步驟都是一樣的,抽出父類,Child.prototype = new Parent();然后重寫里面的步驟(方法)
### 策略模式
>**定義一系列的算法,把它們一個個封裝起來,并且使它們可以相互替換**
**demo:年終獎的薪水的幾倍,是按照一個個等級來劃分的,A級別是3倍,B級別是2倍,C級別是1倍,那么就可以寫三個等級方法,然后封裝在一個方法里,傳入薪水和等級就ok了**
### 發布訂閱模式介紹
>**發布—訂閱模式又叫觀察者模式,它定義了對象間的一種一對多的關系,讓多個觀察者對象同時監聽某一個主題對象,當一個對象發生改變時,所有依賴于它的對象都將得到通知。**
**demo: 比如你向買房,只要把手機給房產中介,房產中介一有消息就發布消息。**
```
var list = {
arr: [],
subscribe: function(fn) {
this.arr.push(fn);
},
notify: function() {
this.arr.forEach(fn => fn());
}
};
var fn1 = function() {
console.log(1)
}
var fn2 = function() {
console.log(2)
}
list.subscribe(fn1);
list.subscribe(fn2);
list.notify();
```
### 中介者模式
>中介者模式的作用是解除對象與對象之間的耦合關系,增加一個中介對象后,所有的相關對象都通過中介者對象來通信,而不是相互引用,所以當一個對象發送改變時,只需要通知中介者對象即可。中介者使各個對象之間耦合松散,而且可以獨立地改變它們之間的交互。
demo:賣手機,顏色和數量判斷加入購物車按鈕是否可用
### 裝飾者模式
>**動態的給類或對象增加職責的設計模式。**
裝飾器模式并不去深入依賴于對象是如何創建的,而是專注于擴展它們的功能這一問題上。裝飾器模式相比生成子類更為靈活。
```
var Car = function() {}
Car.prototype.drive = function() {
console.log('乞丐版');
}
var AutopilotDecorator = function(car) {
this.car = car;
}
AutopilotDecorator.prototype.drive = function() {
this.car.drive();
console.log('啟動自動駕駛模式');
}
var car = new Car();
car = new AutopilotDecorator(car);
car.drive(); //乞丐版;啟動自動駕駛模式;
```
### 適配器模式
>**適配器模式主要解決兩個接口之間不匹配的問題,不會改變原有的接口,而是由一個對象對另一個對象的包裝。
demo:兩個地圖(2個類),他們有一個共同方法但是名字不同,這時候需要定義適配器類, 對其中的一個類進行封裝。**
```
class GooleMap {
show() {
console.log('渲染谷歌地圖')
}
}
class BaiduMap {
display() {
console.log('渲染百度地圖')
}
}
// 定義適配器類, 對BaiduMap類進行封裝
class BaiduMapAdapter {
show() {
var baiduMap = new BaiduMap()
return baiduMap.display()
}
}
function render(map) {
map.show()
}
render(new GooleMap()) // 渲染谷歌地圖
render(new BaiduMapAdapter()) // 渲染百度地圖
```
## canvas和svg
**Canvas**
描述:
通過Javascript來繪制2D圖形。
是逐像素進行渲染的。
其位置發生改變,會重新進行繪制。
**SVG**
描述:
一種使用XML描述的2D圖形的語言
SVG基于XML意味著,SVG DOM中的每個元素都是可用的,可以為某個元素附加Javascript事件處理器。
在 SVG 中,每個被繪制的圖形均被視為對象。如果 SVG 對象的屬性發生變化,那么瀏覽器能夠自動重現圖形。
比較
**Canvas:**
依賴分辨率
不支持事件處理器
弱的文本渲染能力
能夠以 .png 或 .jpg 格式保存結果圖像
最適合圖像密集型的游戲,其中的許多對象會被頻繁重繪
**SVG:**
不依賴分辨率
支持事件處理器
最適合帶有大型渲染區域的應用程序(比如谷歌地圖)
復雜度高會減慢渲染速度(任何過度使用 DOM 的應用都不快)
不適合游戲應用
區別:
SVG與canvas的區別
1. SVG是用來描述XML中2D圖形的語言,canvas借助JavaScript動態描繪2D圖形
2. SVG可支持事件處理程序而canvas不支持
3. SVG中屬性改變時,瀏覽器可以重新呈現它,適用于矢量圖,而canvas不可以,更適合視頻游戲等。
4. canvas可以很好的繪制像素,用于保存結果為png或者gif,可做為API容器。
5. canvas取決于分辨率。SVG與分辨率無關。
6. SVG具有更好的文本渲染,而Canvas不能很好的渲染,渲染中的SVG可能比Canvas慢,特別是應用了大量的DOM。
7. 畫布更適合渲染較小的區域。SVG渲染更好的更大區域。
## ES6模塊與CommonJS模塊有什么區別?
ES6 Module和CommonJS模塊的區別:
* CommonJS是對模塊的淺拷貝,ES6 Module是對模塊的引用,即ES6 Module只存只讀,不能改變其值,具體點就是指針指向不能變,類似const
* import的接口是read-only(只讀狀態),不能修改其變量值。 即不能修改其變量的指針指向,但可以改變變量內部指針指向,可以對commonJS對重新賦值(改變指針指向),但是對ES6 Module賦值會編譯報錯。
ES6 Module和CommonJS模塊的共同點:
* CommonJS和ES6 Module都可以對引入的對象進行賦值,即對對象內部屬性的值進行改變。
### commonjs module和es6 module
(https://www.cnblogs.com/xjy20170907/p/12753635.html)
https://www.cnblogs.com/xjy20170907/p/12753635.html
> 詳解請移步[ES6模塊與CommonJS模塊的差異](http://es6.ruanyifeng.com/#docs/module-loader#ES6-%E6%A8%A1%E5%9D%97%E4%B8%8E-CommonJS-%E6%A8%A1%E5%9D%97%E7%9A%84%E5%B7%AE%E5%BC%82)
## js有哪些類型?
JavaScript的類型分為兩大類,一類是原始類型,一類是復雜(引用)類型。
原始類型:
* boolean
* null
* undefined
* number
* string
* symbol
復雜類型:
* Object
還有一個沒有正式發布但即將被加入標準的原始類型BigInt。
## 為什么會有BigInt的提案?
JavaScript中Number.MAX\_SAFE\_INTEGER表示最大安全數字,計算結果是9007199254740991,即在這個數范圍內不會出現精度丟失(小數除外)。
但是一旦超過這個范圍,js就會出現計算不準確的情況,這在大數計算的時候不得不依靠一些第三方庫進行解決,因此官方提出了BigInt來解決此問題。
## null與undefined的區別是什么?
null表示為空,代表此處不應該有值的存在,一個對象可以是null,代表是個空對象,而null本身也是對象。
undefined表示『不存在』,JavaScript是一門動態類型語言,成員除了表示存在的空值外,還有可能根本就不存在(因為存不存在只在運行期才知道),這就是undefined的意義所在。
## 0.1+0.2為什么不等于0.3?

JS 的`Number`類型遵循的是 IEEE 754 標準,使用的是 64 位固定長度來表示。
IEEE 754 浮點數由三個域組成,分別為 sign bit (符號位)、exponent bias (指數偏移值) 和 fraction (分數值)。64 位中,sign bit 占 1 位,exponent bias 占 11 位,fraction 占 52 位。
通過公式表示浮點數的值**value = sign x exponent x fraction**
\*\*
當一個數為正數,sign bit 為 0,當為負數時,sign bit 為 1.
以 0.1 轉換為 IEEE 754 標準表示為例解釋一下如何求 exponent bias 和 fraction。轉換過程主要經歷 3 個過程:
1. 將 0.1 轉換為二進制表示
2. 將轉換后的二進制通過科學計數法表示
3. 將通過科學計數法表示的二進制轉換為 IEEE 754 標準表示
### 將 0.1 轉換為二進制表示
回顧一下一個數的小數部分如何轉換為二進制。一個數的小數部分,乘以 2,然后取整數部分的結果,再用計算后的小數部分重復計算,直到小數部分為 0 。
因此 0.1 轉換為二進制表示的過程如下:
| 小數 | x2 的結果 | 整數部分 |
| --- | --- | --- |
| 0.1 | 0.2 | 0 |
| 0.2 | 0.4 | 0 |
| 0.4 | 0.8 | 0 |
| 0.8 | 1.6 | 1 |
| 0.6 | 1.2 | 1 |
| 0.2 | 0.4 | 0 |
| 0.4 | 0.8 | 0 |
| 0.8 | 1.6 | 1 |
| 0.6 | 1.2 | 1 |
| ... | ... | ... |
得到 0.1 的二進制表示為 0.00011...(無限重復 0011)
### 通過科學計數法表示
0.00011...(無限重復 0011) 通過科學計數法表示則是 1.10011001...(無線重復 1001)\*2
### 轉換為 IEEE 754 標準表示
當經過科學計數法表示之后,就可以求得 exponent bias 和 fraction 了。
exponent bias (指數偏移值)**等于**雙精度浮點數**固定偏移值**(2-1) 加上指數實際值(即 2 中的 -4) 的**11 位二進制表示**。為什么是 11 位?因為 exponent bias 在 64 位中占 11 位。
因此 0.1 的 exponent bias**等于**1023 + (-4) = 1019 的11 位二進制表示,即 011 1111 1011。
再來獲取 0.1 的 fraction,fraction 就是 1.10011001...(無線重復 1001) 中的小數位,由于 fraction 占 52位所以抽取 52 位小數,1001...(中間有 11 個 1001)...1010**(請注意最后四位,是 1010 而不是 1001,因為四舍五入有進位,這個進位就是造成 0.1 + 0.2 不等于 0.3 的原因)**
~~~
0 011 1111 1011 1001...( 11 x 1001)...1010
(sign bit) (exponent bias) (fraction)
~~~
此時如果將這個數轉換為十進制,可以發現值已經變為 0.100000000000000005551115123126 而不是 0.1 了,因此這個計算精度就出現了問題。
### 解決JS浮點數運算結果不精確的Bug
https://juejin.cn/post/6844903903071322119
## 類型轉換的規則有哪些?
在if語句、邏輯語句、數學運算邏輯、==等情況下都可能出現隱士類型轉換。

## 類型轉換的原理是什么?
**類型轉換**指的是將一種類型轉換為另一種類型,例如:
~~~
var b = 2;
var a = String(b);
console.log(typeof a); //string
~~~
當然,**類型轉換**分為顯式和隱式,但是不管是隱式轉換還是顯式轉換,都會遵循一定的原理,由于JavaScript是一門動態類型的語言,可以隨時賦予任意值,但是各種運算符或條件判斷中是需要特定類型的,因此JavaScript引擎會在運算時為變量設定類型.
這看起來很美好,JavaScript引擎幫我們搞定了`類型`的問題,但是引擎畢竟不是ASI(超級人工智能),它的很多動作會跟我們預期相去甚遠,我們可以從一到面試題開始.
~~~
{}+[] //0
~~~
答案是0
是什么原因造成了上述結果呢?那么我們得從ECMA-262中提到的轉換規則和抽象操作說起,有興趣的童鞋可以仔細閱讀下這浩如煙海的[語言規范](http://ecma-international.org/ecma-262/5.1/),如果沒這個耐心還是往下看.
這是JavaScript種類型轉換可以從**原始類型**轉為**引用類型**,同樣可以將**引用類型**轉為**原始類型**,轉為原始類型的抽象操作為`ToPrimitive`,而后續更加細分的操作為:`ToNumber ToString ToBoolean`。
為了更深入的探究JavaScript引擎是如何處理代碼中類型轉換問題的,就需要看 ECMA-262詳細的規范,從而探究其內部原理,我們從這段內部原理示意代碼開始.
~~~
// ECMA-262, section 9.1, page 30. Use null/undefined for no hint,
// (1) for number hint, and (2) for string hint.
function ToPrimitive(x, hint) {
// Fast case check.
if (IS_STRING(x)) return x;
// Normal behavior.
if (!IS_SPEC_OBJECT(x)) return x;
if (IS_SYMBOL_WRAPPER(x)) throw MakeTypeError(kSymbolToPrimitive);
if (hint == NO_HINT) hint = (IS_DATE(x)) ? STRING_HINT : NUMBER_HINT;
return (hint == NUMBER_HINT) ? DefaultNumber(x) : DefaultString(x);
}
// ECMA-262, section 8.6.2.6, page 28.
function DefaultNumber(x) {
if (!IS_SYMBOL_WRAPPER(x)) {
var valueOf = x.valueOf;
if (IS_SPEC_FUNCTION(valueOf)) {
var v = %_CallFunction(x, valueOf);
if (IsPrimitive(v)) return v;
}
var toString = x.toString;
if (IS_SPEC_FUNCTION(toString)) {
var s = %_CallFunction(x, toString);
if (IsPrimitive(s)) return s;
}
}
throw MakeTypeError(kCannotConvertToPrimitive);
}
// ECMA-262, section 8.6.2.6, page 28.
function DefaultString(x) {
if (!IS_SYMBOL_WRAPPER(x)) {
var toString = x.toString;
if (IS_SPEC_FUNCTION(toString)) {
var s = %_CallFunction(x, toString);
if (IsPrimitive(s)) return s;
}
var valueOf = x.valueOf;
if (IS_SPEC_FUNCTION(valueOf)) {
var v = %_CallFunction(x, valueOf);
if (IsPrimitive(v)) return v;
}
}
throw MakeTypeError(kCannotConvertToPrimitive);
}
~~~
上面代碼的邏輯是這樣的:
1. 如果變量為字符串,直接返回.
2. 如果`!IS_SPEC_OBJECT(x)`,直接返回.
3. 如果`IS_SYMBOL_WRAPPER(x)`,則拋出異常.
4. 否則會根據傳入的`hint`來調用`DefaultNumber`和`DefaultString`,比如如果為`Date`對象,會調用`DefaultString`.
5. `DefaultNumber`:首`先x.valueOf`,如果為`primitive`,則返回`valueOf`后的值,否則繼續調用`x.toString`,如果為`primitive`,則返回`toString`后的值,否則拋出異常
6. `DefaultString`:和`DefaultNumber`正好相反,先調用`toString`,如果不是`primitive`再調用`valueOf`.
那講了實現原理,這個`ToPrimitive`有什么用呢?實際很多操作會調用`ToPrimitive`,比如加、相等或比較操。在進行加操作時會將左右操作數轉換為`primitive`,然后進行相加。
下面來個實例,({}) + 1(將{}放在括號中是為了內核將其認為一個代碼塊)會輸出啥?可能日常寫代碼并不會這樣寫,不過網上出過類似的面試題。
加操作只有左右運算符同時為`String或Number`時會執行對應的`%_StringAdd或%NumberAdd`,下面看下`({}) + 1`內部會經過哪些步驟:
`{}`和`1`首先會調用ToPrimitive`{}`會走到`DefaultNumber`,首先會調用`valueOf`,返回的是`Object``{}`,不是primitive類型,從而繼續走到`toString`,返回`[object Object]`,是`String`類型 最后加操作,結果為`[object Object]1`再比如有人問你`[] + 1`輸出啥時,你可能知道應該怎么去計算了,先對`[]`調用`ToPrimitive`,返回空字符串,最后結果為"1"。
## 談談你對原型鏈的理解??
這個問題關鍵在于兩個點,一個是原型對象是什么,另一個是原型鏈是如何形成的
### 原型對象
絕大部分的函數(少數內建函數除外)都有一個`prototype`屬性,這個屬性是原型對象用來創建新對象實例,而所有被創建的對象都會共享原型對象,因此這些對象便可以訪問原型對象的屬性。
例如`hasOwnProperty()`方法存在于Obejct原型對象中,它便可以被任何對象當做自己的方法使用.
> 用法:`object.hasOwnProperty( propertyName )`
> `hasOwnProperty()`函數的返回值為`Boolean`類型。如果對象`object`具有名稱為`propertyName`的屬性,則返回`true`,否則返回`false`。
~~~
var person = {
name: "Messi",
age: 29,
profession: "football player"
};
console.log(person.hasOwnProperty("name")); //true
console.log(person.hasOwnProperty("hasOwnProperty")); //false
console.log(Object.prototype.hasOwnProperty("hasOwnProperty")); //true
~~~
由以上代碼可知,`hasOwnProperty()`并不存在于`person`對象中,但是`person`依然可以擁有此方法.
所以`person`對象是如何找到`Object`對象中的方法的呢?靠的是原型鏈。
### 原型鏈
原因是每個對象都有`__proto__`屬性,此屬性指向該對象的構造函數的原型。
對象可以通過`__proto__`與上游的構造函數的原型對象連接起來,而上游的原型對象也有一個`__proto__`,這樣就形成了原型鏈。
> 經典原型鏈圖

## 如何判斷是否是數組?
es6中加入了新的判斷方法
~~~
if(Array.isArray(value)){
return true;
}
~~~
在考慮兼容性的情況下可以用toString的方法
~~~
if(!Array.isArray){
Array.isArray = function(arg){
return Object.prototype.toString.call(arg)==='[object Array]'
}
}
~~~
## 談一談你對this的了解??
this的指向不是在編寫時確定的,而是在執行時確定的,同時,this不同的指向在于遵循了一定的規則。
首先,在默認情況下,this是指向全局對象的,比如在瀏覽器就是指向window。
~~~
name = "Bale";
function sayName () {
console.log(this.name);
};
sayName(); //"Bale"
~~~
其次,如果函數被調用的位置存在上下文對象時,那么函數是被隱式綁定的。
~~~
function f() {
console.log( this.name );
}
var obj = {
name: "Messi",
f: f
};
obj.f(); //被調用的位置恰好被對象obj擁有,因此結果是Messi
~~~
再次,顯示改變this指向,常見的方法就是call、apply、bind
以bind為例:
~~~
function f() {
console.log( this.name );
}
var obj = {
name: "Messi",
};
var obj1 = {
name: "Bale"
};
f.bind(obj)(); //Messi ,由于bind將obj綁定到f函數上后返回一個新函數,因此需要再在后面加上括號進行執行,這是bind與apply和call的區別
~~~
最后,也是優先級最高的綁定 new 綁定。
用 new 調用一個構造函數,會創建一個新對象, 在創造這個新對象的過程中,新對象會自動綁定到Person對象的this上,那么 this 自然就指向這個新對象。
~~~
function Person(name) {
this.name = name;
console.log(name);
}
var person1 = new Person('Messi'); //Messi
~~~
> 綁定優先級: new綁定 > 顯式綁定 >隱式綁定 >默認綁定
## 那么箭頭函數的this指向哪里??
箭頭函數不同于傳統JavaScript中的函數,箭頭函數并沒有屬于自己的this,它的所謂的this是捕獲其所在上下文的 this 值,作為自己的 this 值,并且由于沒有屬于自己的this,而箭頭函數是不會被new調用的,這個所謂的this也不會被改變.
我們可以用Babel理解一下箭頭函數:
~~~
// ES6
const obj = {
getArrow() {
return () => {
console.log(this === obj);
};
}
}
~~~
轉化后
~~~
// ES5,由 Babel 轉譯
var obj = {
getArrow: function getArrow() {
var _this = this;
return function () {
console.log(_this === obj);
};
}
};
~~~
## async/await是什么?
async 函數,就是 Generator 函數的語法糖,它建立在Promises上,并且與所有現有的基于Promise的API兼容。
1. Async—聲明一個異步函數(async function someName(){...})
* 自動將常規函數轉換成Promise,返回值也是一個Promise對象
* 只有async函數內部的異步操作執行完,才會執行then方法指定的回調函數
* 異步函數內部可以使用await
2. Await—暫停異步的功能執行(var result = await someAsyncCall()??
* 放置在Promise調用之前,await強制其他代碼等待,直到Promise完成并返回結果
* 只能與Promise一起使用,不適用與回調
* 只能在async函數內部使用
## async/await相比于Promise的優勢?
* 代碼讀起來更加同步,Promise雖然擺脫了回調地獄,但是then的鏈式調用也會帶來額外的閱讀負擔
* Promise傳遞中間值非常麻煩,而async/await幾乎是同步的寫法,非常優雅
* 錯誤處理友好,async/await可以用成熟的try/catch,Promise的錯誤捕獲非常冗余
* 調試友好,Promise的調試很差,由于沒有代碼塊,你不能在一個返回表達式的箭頭函數中設置斷點,如果你在一個.then代碼塊中使用調試器的步進(step-over)功能,調試器并不會進入后續的.then代碼塊,因為調試器只能跟蹤同步代碼的『每一步』。
## JavaScript的參數是按照什么方式傳遞的?
### 基本類型傳遞方式
由于js中存在**復雜類型**和**基本類型**,對于**基本類型**而言,是按值傳遞的.
~~~
var a = 1;
function test(x) {
x = 10;
console.log(x);
}
test(a); // 10
console.log(a); // 1
~~~
雖然在函數`test`中`a`被修改,并沒有有影響到 外部`a`的值,基本類型是按值傳遞的.
### 復雜類型按引用傳遞?
我們將外部`a`作為一個對象傳入`test`函數.
~~~
var a = {
a: 1,
b: 2
};
function test(x) {
x.a = 10;
console.log(x);
}
test(a); // { a: 10, b: 2 }
console.log(a); // { a: 10, b: 2 }
~~~
可以看到,在函數體內被修改的`a`對象也同時影響到了外部的`a`對象,可見復雜類型是按**引用傳遞的**.
可是如果再做一個實驗:
~~~
var a = {
a: 1,
b: 2
};
function test(x) {
x = 10;
console.log(x);
}
test(a); // 10
console.log(a); // { a: 1, b: 2 }
~~~
外部的`a`并沒有被修改,如果是按引用傳遞的話,由于共享同一個堆內存,`a`在外部也會表現為`10`才對. 此時的復雜類型同時表現出了`按值傳遞`和`按引用傳遞`的特性.
### 按共享傳遞
復雜類型之所以會產生這種特性,原因就是在傳遞過程中,對象`a`先產生了一個`副本a`,這個`副本a`并不是深克隆得到的`副本a`,`副本a`地址同樣指向對象`a`指向的堆內存.

因此在函數體中修改`x=10`只是修改了`副本a`,`a`對象沒有變化. 但是如果修改了`x.a=10`是修改了兩者指向的同一堆內存,此時對象`a`也會受到影響.
有人講這種特性叫做**傳遞引用**,也有一種說法叫做**按共享傳遞**.
## 下劃線轉換駝峰
```
toHump(name)?{
????????return?name.replace(/\_(\w)/g,?function(all,?letter){
????????????return?letter.toUpperCase();
????????});
????}
```
## 駝峰轉換下劃線
```
toLine(name)?{
??????return?name.replace(/([A-Z])/g,"_$1").toLowerCase();
????}
```
https://www.cnblogs.com/webSong/p/10113556.html
## 聊一聊如何在JavaScript中實現不可變對象?
實現不可變數據有三種主流的方法
1. 深克隆,但是深克隆的性能非常差,不適合大規模使用
2. Immutable.js,Immutable.js是自成一體的一套數據結構,性能良好,但是需要學習額外的API
3. immer,利用Proxy特性,無需學習額外的api,性能良好
4. Object.preventExtensions() 防止擴展
此方法可防止向現有對象添加新屬性,`preventExtensions()` 是不可逆的操作,我們永遠不能再向對象添加額外的屬性。
```
const myTesla = {
maxSpeed: 155,
batteryLife: 300,
weight: 2300
};
```
~~~
Object.isExtensible(myTesla); // true
Object.preventExtensions(myTesla);
Object.isExtensible(myTesla); // false
myTesla.color = 'blue';
console.log(myTesla.color) // undefined
~~~
5. Object.seal() 密封
它可以防止添加或刪除屬性,`seal()` 還可以防止修改屬性描述符。
~~~
Object.isSealed(myTesla); // false
Object.seal(myTesla);
Object.isSealed(myTesla); // true
myTesla.color = 'blue';
console.log(myTesla.color); // undefined
delete myTesla.batteryLife; // false
console.log(myTesla.batteryLife); // 300
Object.defineProperty(myTesla, 'batteryLife'); // TypeError: Cannot redefine property: batteryLife
~~~
6. Object.freeze() 凍結
它的作用與 `Object.seal()` 相同,而且它使屬性不可寫。
~~~
Object.isFrozen(myTesla); // false
Object.freeze(myTesla);
Object.isFrozen(myTesla); // true
myTesla.color = 'blue';
console.log(myTesla.color); // undefined
delete myTesla.batteryLife;
console.log(myTesla.batteryLife); // 300
Object.defineProperty(myTesla, 'batteryLife'); // TypeError: Cannot redefine property: batteryLife
myTesla.batteryLife = 400;
console.log(myTesla.batteryLife); // 300
~~~
> 原理詳解請移步[實現JavaScript不可變數據](https://www.cxymsg.com/guide/jsBasic.html#immuatble)
## JavaScript的基本類型和復雜類型是儲存在哪里的?
基本類型儲存在棧中,但是一旦被閉包引用則成為常住內存,會儲存在內存堆中。
復雜類型會儲存在內存堆中。
> 原理解析請移步[JavaScript內存管理](https://blog.csdn.net/huangpb123/article/details/103791666)
## 講講JavaScript垃圾回收是怎么做的?
此過程比較復雜,請看詳細解析。
> 原理解析請移步[JavaScript內存管理](https://blog.csdn.net/huangpb123/article/details/103791666)
* * *
### break,continue和return的用法及區別
**相同之處:** 三個都會將此時進行的語句停止。
**不同之處:**
1、break:是立即結束語句,并跳出語句,進行下個語句執行。
2、continue:是停止當前語句,并從頭執行該語句。
3、return:停止函數。
4、使用的語句環境不一樣,break和continue是用在循環或switch語句中,return是用在函數語句中。
https://www.cnblogs.com/itgezhu/p/11226852.html
## 純函數和函數柯里化
https://blog.csdn.net/gjsiaifa/article/details/106861147
### 詳解JS函數柯里化
https://www.jianshu.com/p/2975c25e4d71
## localStorage 封裝過期控制代碼
```
// 封裝過期控制代碼
function localStorageSet(key1, value){
//獲取時間戳
var curTime = new Date().getTime();
return localStorage.setItem(key1, JSON.stringify({
data: value,
time: curTime
}));
}
var state = 123;
localStorageSet("bb", state); //存儲時間戳,和存儲數據;
function localStorageGet(key1, exp) {
var data = localStorage.getItem(key1);
console.log(data)
//轉為對象
var dataObj = JSON.parse(data);
console.log(dataObj)
if(new Date().getTime() - dataObj.time > exp) {
alert("信息已過期");
} else {
alert("信息沒過期");
var dataObjDatatoJson = JSON.parse(dataObj.data)
return dataObjDatatoJson;
}
}
let remenber = document.querySelector(".remenber");
remenber.addEventListener("click", () => {
localStorageGet("bb", 5000);
})
~~~
```
## for、forEach、map數組遍歷區別和性能比較
先上結果:[遍歷](https://so.csdn.net/so/search?q=%E9%81%8D%E5%8E%86&spm=1001.2101.3001.7020)時間上**for循環遍歷 < for…of遍歷 < forEach遍歷 < for…in遍歷 < map遍歷**
### **背景**
常用的[數組](https://so.csdn.net/so/search?q=%E6%95%B0%E7%BB%84&spm=1001.2101.3001.7020)遍歷方式有很多,
如最經典的for循環
~~~
for (var i = 0; i < arr.length; i++) {}
~~~
再者有了for…in
~~~
for (var i in arr) {}
~~~
forEach
~~~
arr.forEach(function (i) {});
~~~
map
~~~
arr.map(function (i) {});
~~~
然后ES6有了更為方便的for…of
~~~
for (let i of arr) {}
~~~
### **區別**
forEach 遍歷列表值,不能使用 break 語句或使用 return 語句
for in 遍歷對象鍵值(key),或者數組下標,不推薦循環一個數組
for of 遍歷列表值,允許遍歷 Arrays(數組), Strings(字符串), Maps(映射), Sets(集合)等可迭代的數據結構等.在 ES6 中引入的 for of 循環,以替代 for in 和 forEach() ,并支持新的迭代協議。
for in循環出的是key,for of循環出的是value;
for of是ES6新引入的特性。修復了ES5的for in的不足;
for of不能循環普通的對象,需要通過和Object.keys()搭配使用。
### **性能比較**
注:filter、every、some跟forEach/map相近。
#### 1.**對比方案**
本次采用最直觀的方式進行對比:通過對高數量級數組的遍歷時間進行比較。
##### 1.1 數組arr:
~~~
let arr = [];
for (let i = 0; i < 10000000; i++) {
arr[i] = i;
}
console.log(arr); // [0, 1, 2, 3, ... , 9999999]
~~~
##### 1.2 對比函數:
~~~
function getAverageTime (cb) {
let _start = +new Date();
for (let k = 0; k < 20; k++) {
cb(); // 遍歷函數
}
return (+new Date() - _start) / 20 + 'ms'
}
~~~
* 其中cb為遍歷函數。
我們通過20次求平均值的方式來推算遍歷的時間,以此作為比較依據。
#### 2.**比較**
##### 2.1 經典的for循環遍歷
~~~
getAverageTime(function () {
for (let i = 0; i < arr.length; i++) {
let item = arr[i];
// ...
}
})
~~~
結果:
6.3ms

##### 2.2 for…in遍歷
~~~
getAverageTime(function () {
for (let i in arr) {
let item = arr[i];
// ...
}
})
~~~
結果:
1539.45ms

##### 2.3 forEach遍歷
~~~
getAverageTime(function () {
arr.forEach(item => {})
})
~~~
結果:
190.75ms

##### 2.4 map遍歷
~~~
getAverageTime(function () {
arr.map(item => {})
})
~~~
結果:
2014.65ms

##### 2.5 for…of遍歷
~~~
getAverageTime(function () {
for (let item of arr) {
// ...
}
})
~~~
結果:
129.5ms

##### babel轉ES5后遍歷
for…of是ES6語法,所以日常頁面中基本會babel轉換,所以需要測試一下轉換后的遍歷
~~~
getAverageTime(function () {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = arr[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
//...
var item = _step.value;
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
});
~~~
結果:
105.9ms

(是不是感覺for…of經過Babel轉換后的代碼很詭異,有興趣可以去了解下[Symbol對象](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol),其實Symbol對象也是ES6所新加的,只是兼容比for…of好些,要兼容低版本手機的話了解一下[es-symbol](https://www.npmjs.com/package/es-symbol))
#### 3 **結果分析**
通過對比可知,遍歷時間
~~~
for循環遍歷 < for...of遍歷 < forEach遍歷 < for...in遍歷 < map遍歷
~~~
##### 3.1 \*為何for… in會慢?
因為for … in語法是第一個能夠迭代對象鍵的JavaScript語句,循環對象鍵({})與在數組(\[\])上進行循環不同,引擎會執行一些額外的工作來跟蹤已經迭代的屬性。
因此可以大致可以得出以下幾點:
* 數據量大時,遍歷性能的差距尤為明顯;
* for系遍歷總體性能好于forEach、map等數組方法
* 你大爺畢竟是你大爺,性能最佳的還是經典的for循環遍歷
* forEach性能優于map
* for…of要兼容低版本設備的話還是算了
遍歷的性能可以作為以后開發的參考,畢竟數據量不大的時候差異可忽略不計,更多的可以根據實際作用來考慮遍歷方式,比方說for系遍歷可以break中途退出而forEach/map不行。
##### 附上考慮變量類型的遍歷抽象函數
~~~
/**
* @param {Object | Array} array
* @param {Function} func
*/
function eachPlus (array, func) {
if (!array) {
return;
}
let rst = void 0,
type = Object.prototype.toString.call(array).replace(/\[object\s|\]/g, '');
if (type === 'Array') {
for (let i = 0, len = array.length; i < len; i++) {
rst = func(array[i], i);
if (rst === false) { // 實現break中斷
break;
}
}
} else if (type === 'Object') {
for (let k in array) {
if (array.hasOwnProperty(k)) {
rst = func(array[k], k);
if (rst === false) {
break;
}
}
}
}
}
// demo
eachPlus([1, 2, 3], item => {
console.log(item);
})
eachPlue({ a: 1, b: 2, c: 3 }, item => {
console.log(item);
})
~~~
## 公眾號
想要實時關注筆者最新的文章和最新的文檔更新請關注公眾號**程序員面試官**,后續的文章會優先在公眾號更新.
**簡歷模板**:關注公眾號回復「模板」獲取
《**前端面試手冊**》:配套于本指南的突擊手冊,關注公眾號回復「fed」獲取

- 前言
- 指南使用手冊
- 為什么會有這個項目
- 面試技巧
- 面試官到底想看什么樣的簡歷?
- 面試回答問題的技巧
- 如何通過HR面
- 推薦
- 書籍/課程推薦
- 前端基礎
- HTML基礎
- CSS基礎
- JavaScript基礎
- 瀏覽器與新技術
- DOM
- 前端基礎筆試
- HTTP筆試部分
- JavaScript筆試部分
- 前端原理詳解
- JavaScript的『預解釋』與『變量提升』
- Event Loop詳解
- 實現不可變數據
- JavaScript內存管理
- 實現深克隆
- 如何實現一個Event
- JavaScript的運行機制
- 計算機基礎
- HTTP協議
- TCP面試題
- 進程與線程
- 數據結構與算法
- 算法面試題
- 字符串類面試題
- 前端框架
- 關于前端框架的面試須知
- Vue面試題
- React面試題
- 框架原理詳解
- 虛擬DOM原理
- Proxy比defineproperty優劣對比?
- setState到底是異步的還是同步的?
- 前端路由的實現
- redux原理全解
- React Fiber 架構解析
- React組件復用指南
- React-hooks 抽象組件
- 框架實戰技巧
- 如何搭建一個組件庫的開發環境
- 組件設計原則
- 實現輪播圖組件
- 性能優化
- 前端性能優化-加載篇
- 前端性能優化-執行篇
- 工程化
- webpack面試題
- 前端工程化
- Vite
- 安全
- 前端安全面試題
- npm
- 工程化原理
- 如何寫一個babel
- Webpack HMR 原理解析
- webpack插件編寫
- webpack 插件化設計
- Webpack 模塊機制
- webpack loader實現
- 如何開發Babel插件
- git
- 比較
- 查看遠程倉庫地址
- git flow
- 比較分支的不同并保存壓縮文件
- Tag
- 回退
- 前端項目經驗
- 確定用戶是否在當前頁面
- 前端下載文件
- 只能在微信中訪問
- 打開新頁面-被瀏覽器攔截
- textarea高度隨內容變化 vue版
- 去掉ios原始播放大按鈕
- nginx在MAC上的安裝、啟動、重啟和關閉
- 解析latex格式的數學公式
- 正則-格式化a鏈接
- 封裝的JQ插件庫
- 打包問題總結
- NPM UI插件
- 帶你入門前端工程
- webWorker+indexedDB性能優化
- 多個相鄰元素切換效果出現邊框重疊問題的解決方法
- 監聽前端storage變化