摘要:本文挑選了到大廠面試題,大家在閱讀時,建議不要先看我的答案,而是自己先思考一番。構造函數返回值是或,是返回的是種返回的對象。
今年來,各大公司都縮減了HC,甚至是采取了“裁員”措施,在這樣的大環境之下,想要獲得一份更好的工作,必然需要付出更多的努力。
本文挑選了20到大廠面試題,大家在閱讀時,建議不要先看我的答案,而是自己先思考一番。盡管,本文所有的答案,都是我在翻閱各種資料,思考并驗證之后,才給出的。但因水平有限,本人的答案未必是最優的,如果您有更好的答案,歡迎給我留言。
本文篇幅較長,希望小伙伴們能夠堅持讀完,如果想加入交流群,可以通過文末的公眾號添加我為好友。
更多優質文章可戳: https://github.com/YvetteLau/...
1. new的實現原理是什么?new 的實現原理:
創建一個空對象,構造函數中的this指向這個空對象
這個新對象被執行 [[原型]] 連接
執行構造函數方法,屬性和方法被添加到this引用的對象中
如果構造函數中沒有返回其它對象,那么返回this,即創建的這個的新對象,否則,返回構造函數中返回的對象。
function _new() { let target = {}; //創建的新對象 //第一個參數是構造函數 let [constructor, ...args] = [...arguments]; //執行[[原型]]連接;target 是 constructor 的實例 target.__proto__ = constructor.prototype; //執行構造函數,將屬性或方法添加到創建的空對象上 let result = constructor.apply(target, args); if (result && (typeof (result) == "object" || typeof (result) == "function")) { //如果構造函數執行的結構返回的是一個對象,那么返回這個對象 return result; } //如果構造函數返回的不是一個對象,返回創建的新對象 return target; }2. 如何正確判斷this的指向?
如果用一句話說明 this 的指向,那么即是: 誰調用它,this 就指向誰。
但是僅通過這句話,我們很多時候并不能準確判斷 this 的指向。因此我們需要借助一些規則去幫助自己:
this 的指向可以按照以下順序判斷:
全局環境中的 this瀏覽器環境:無論是否在嚴格模式下,在全局執行環境中(在任何函數體外部)this 都指向全局對象 window;
node 環境:無論是否在嚴格模式下,在全局執行環境中(在任何函數體外部),this 都是空對象 {};
是否是 new 綁定如果是 new 綁定,并且構造函數中沒有返回 function 或者是 object,那么 this 指向這個新對象。如下:
構造函數返回值不是 function 或 object。new Super() 返回的是 this 對象。
function Super(age) { this.age = age; } let instance = new Super("26"); console.log(instance.age); //26
構造函數返回值是 function 或 object,new Super()是返回的是Super種返回的對象。
function Super(age) { this.age = age; let obj = {a: "2"}; return obj; } let instance = new Super("hello"); console.log(instance);//{ a: "2" } console.log(instance.age); //undefined函數是否通過 call,apply 調用,或者使用了 bind 綁定,如果是,那么this綁定的就是指定的對象【歸結為顯式綁定】。
function info(){ console.log(this.age); } var person = { age: 20, info } var age = 28; var info = person.info; info.call(person); //20 info.apply(person); //20 info.bind(person)(); //20
這里同樣需要注意一種特殊情況,如果 call,apply 或者 bind 傳入的第一個參數值是 undefined 或者 null,嚴格模式下 this 的值為傳入的值 null /undefined。非嚴格模式下,實際應用的默認綁定規則,this 指向全局對象(node環境為global,瀏覽器環境為window)
function info(){ //node環境中:非嚴格模式 global,嚴格模式為null //瀏覽器環境中:非嚴格模式 window,嚴格模式為null console.log(this); console.log(this.age); } var person = { age: 20, info } var age = 28; var info = person.info; //嚴格模式拋出錯誤; //非嚴格模式,node下輸出undefined(因為全局的age不會掛在 global 上) //非嚴格模式。瀏覽器環境下輸出 28(因為全局的age會掛在 window 上) info.call(null);隱式綁定,函數的調用是在某個對象上觸發的,即調用位置上存在上下文對象。典型的隱式調用為: xxx.fn()
function info(){ console.log(this.age); } var person = { age: 20, info } var age = 28; person.info(); //20;執行的是隱式綁定默認綁定,在不能應用其它綁定規則時使用的默認規則,通常是獨立函數調用。
非嚴格模式: node環境,執行全局對象 global,瀏覽器環境,執行全局對象 window。
嚴格模式:執行 undefined
function info(){ console.log(this.age); } var age = 28; //嚴格模式;拋錯 //非嚴格模式,node下輸出 undefined(因為全局的age不會掛在 global 上) //非嚴格模式。瀏覽器環境下輸出 28(因為全局的age會掛在 window 上) //嚴格模式拋出,因為 this 此時是 undefined info();箭頭函數的情況:
箭頭函數沒有自己的this,繼承外層上下文綁定的this。
let obj = { age: 20, info: function() { return () => { console.log(this.age); //this繼承的是外層上下文綁定的this } } } let person = {age: 28}; let info = obj.info(); info(); //20 let info2 = obj.info.call(person); info2(); //283. 深拷貝和淺拷貝的區別是什么?實現一個深拷貝
深拷貝和淺拷貝是針對復雜數據類型來說的,淺拷貝只拷貝一層,而深拷貝是層層拷貝。
深拷貝深拷貝復制變量值,對于非基本類型的變量,則遞歸至基本類型變量后,再復制。 深拷貝后的對象與原來的對象是完全隔離的,互不影響,對一個對象的修改并不會影響另一個對象。淺拷貝
淺拷貝是會將對象的每個屬性進行依次復制,但是當對象的屬性值是引用類型時,實質復制的是其引用,當引用指向的值改變時也會跟著變化。
可以使用 for in、 Object.assign、 擴展運算符 ... 、Array.prototype.slice()、Array.prototype.concat() 等,例如:
let obj = { name: "Yvette", age: 18, hobbies: ["reading", "photography"] } let obj2 = Object.assign({}, obj); let obj3 = {...obj}; obj.name = "Jack"; obj.hobbies.push("coding"); console.log(obj);//{ name: "Jack", age: 18,hobbies: [ "reading", "photography", "coding" ] } console.log(obj2);//{ name: "Yvette", age: 18,hobbies: [ "reading", "photography", "coding" ] } console.log(obj3);//{ name: "Yvette", age: 18,hobbies: [ "reading", "photography", "coding" ] }
可以看出淺拷貝只最第一層屬性進行了拷貝,當第一層的屬性值是基本數據類型時,新的對象和原對象互不影響,但是如果第一層的屬性值是復雜數據類型,那么新對象和原對象的屬性值其指向的是同一塊內存地址。
深拷貝實現1.深拷貝最簡單的實現是: JSON.parse(JSON.stringify(obj))
JSON.parse(JSON.stringify(obj)) 是最簡單的實現方式,但是有一些缺陷:
對象的屬性值是函數時,無法拷貝。
原型鏈上的屬性無法拷貝
不能正確的處理 Date 類型的數據
不能處理 RegExp
會忽略 symbol
會忽略 undefined
2.實現一個 deepClone 函數
如果是基本數據類型,直接返回
如果是 RegExp 或者 Date 類型,返回對應類型
如果是復雜數據類型,遞歸。
考慮循環引用的問題
function deepClone(obj, hash = new WeakMap()) { //遞歸拷貝 if (obj instanceof RegExp) return new RegExp(obj); if (obj instanceof Date) return new Date(obj); if (obj === null || typeof obj !== "object") { //如果不是復雜數據類型,直接返回 return obj; } if (hash.has(obj)) { return hash.get(obj); } /** * 如果obj是數組,那么 obj.constructor 是 [Function: Array] * 如果obj是對象,那么 obj.constructor 是 [Function: Object] */ let t = new obj.constructor(); hash.set(obj, t); for (let key in obj) { //遞歸 if (obj.hasOwnProperty(key)) {//是否是自身的屬性 t[key] = deepClone(obj[key], hash); } } return t; }4. call/apply 的實現原理是什么?
call 和 apply 的功能相同,都是改變 this 的執行,并立即執行函數。區別在于傳參方式不同。
func.call(thisArg, arg1, arg2, ...):第一個參數是 this 指向的對象,其它參數依次傳入。
func.apply(thisArg, [argsArray]):第一個參數是 this 指向的對象,第二個參數是數組或類數組。
一起思考一下,如何模擬實現 call ?
首先,我們知道,函數都可以調用 call,說明 call 是函數原型上的方法,所有的實例都可以調用。即: Function.prototype.call。
在 call 方法中獲取調用call()函數
如果第一個參數沒有傳入,那么默認指向 window / global(非嚴格模式)
傳入 call 的第一個參數是 this 指向的對象,根據隱式綁定的規則,我們知道 obj.foo(), foo() 中的 this 指向 obj;因此我們可以這樣調用函數 thisArgs.func(...args)
返回執行結果
Function.prototype.call = function() { let [thisArg, ...args] = [...arguments]; if (!thisArg) { //context為null或者是undefined thisArg = typeof window === "undefined" ? global : window; } //this的指向的是當前函數 func (func.call) thisArg.func = this; //執行函數 let result = thisArg.func(...args); delete thisArg.func; //thisArg上并沒有 func 屬性,因此需要移除 return result; }
bind 的實現思路和 call 一致,僅參數處理略有差別。如下:
Function.prototype.apply = function(thisArg, rest) { let result; //函數返回結果 if (!thisArg) { //context為null或者是undefined thisArg = typeof window === "undefined" ? global : window; } //this的指向的是當前函數 func (func.call) thisArg.func = this; if(!rest) { //第二個參數為 null / undefined result = thisArg.func(); }else { result = thisArg.func(...rest); } delete thisArg.func; //thisArg上并沒有 func 屬性,因此需要移除 return result; }5. 柯里化函數實現
在開始之前,我們首先需要搞清楚函數柯里化的概念。
函數柯里化是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且返回接受余下的參數而且返回結果的新函數的技術。
const curry = (fn, ...args) => args.length < fn.length //參數長度不足時,重新柯里化該函數,等待接受新參數 ? (...arguments) => curry(fn, ...args, ...arguments) //參數長度滿足時,執行函數 : fn(...args);
function sumFn(a, b, c) { return a + b + c; } var sum = curry(sumFn); console.log(sum(2)(3)(5));//10 console.log(sum(2, 3, 5));//10 console.log(sum(2)(3, 5));//10 console.log(sum(2, 3)(5));//10
函數柯里化的主要作用:
參數復用
提前返回 – 返回接受余下的參數且返回結果的新函數
延遲執行 – 返回新函數,等待執行
6. 如何讓 (a == 1 && a == 2 && a == 3) 的值為true?利用隱式類型轉換
== 操作符在左右數據類型不一致時,會先進行隱式轉換。
a == 1 && a == 2 && a == 3 的值意味著其不可能是基本數據類型。因為如果 a 是 null 或者是 undefined bool類型,都不可能返回true。
因此可以推測 a 是復雜數據類型,JS 中復雜數據類型只有 object,回憶一下,Object 轉換為原始類型會調用什么方法?
如果部署了 [Symbol.toPrimitive] 接口,那么調用此接口,若返回的不是基本數據類型,拋出錯誤。
如果沒有部署 [Symbol.toPrimitive] 接口,那么根據要轉換的類型,先調用 valueOf / toString
非Date類型對象,hint 是 default 時,調用順序為:valueOf >>> toString,即valueOf 返回的不是基本數據類型,才會繼續調用 valueOf,如果toString 返回的還不是基本數據類型,那么拋出錯誤。
如果 hint 是 string(Date對象的hint默認是string) ,調用順序為:toString >>> valueOf,即toString 返回的不是基本數據類型,才會繼續調用 valueOf,如果valueOf 返回的還不是基本數據類型,那么拋出錯誤。
如果 hint 是 number,調用順序為: valueOf >>> toString
//部署 [Symbol.toPrimitive] / valueOf/ toString 皆可 //一次返回1,2,3 即可。 let a = { [Symbol.toPrimitive]: (function(hint) { let i = 1; //閉包的特性之一:i 不會被回收 return function() { return i++; } })() }
利用數據劫持(Proxy/Object.definedProperty)
let i = 1; let a = new Proxy({}, { i: 1, get: function () { return () => this.i++; } });
數組的 toString 接口默認調用數組的 join 方法,重新 join 方法
let a = [1, 2, 3]; a.join = a.shift;7. 什么是BFC?BFC的布局規則是什么?如何創建BFC?
Box 是 CSS 布局的對象和基本單位,頁面是由若干個Box組成的。
元素的類型 和 display 屬性,決定了這個 Box 的類型。不同類型的 Box 會參與不同的 Formatting Context。
Formatting Context
Formatting Context 是頁面的一塊渲染區域,并且有一套渲染規則,決定了其子元素將如何定位,以及和其它元素的關系和相互作用。
Formatting Context 有 BFC (Block formatting context),IFC (Inline formatting context),FFC (Flex formatting context) 和 GFC (Grid formatting context)。FFC 和 GFC 為 CC3 中新增。
BFC布局規則
BFC內,盒子依次垂直排列。
BFC內,兩個盒子的垂直距離由 margin 屬性決定。屬于同一個BFC的兩個相鄰Box的margin會發生重疊【符合合并原則的margin合并后是使用大的margin】
BFC內,每個盒子的左外邊緣接觸內部盒子的左邊緣(對于從右到左的格式,右邊緣接觸)。即使在存在浮動的情況下也是如此。除非創建新的BFC。
BFC的區域不會與float box重疊。
BFC就是頁面上的一個隔離的獨立容器,容器里面的子元素不會影響到外面的元素。反之也如此。
計算BFC的高度時,浮動元素也參與計算。
如何創建BFC
根元素
浮動元素(float 屬性不為 none)
position 為 absolute 或 fixed
overflow 不為 visible 的塊元素
display 為 inline-block, table-cell, table-caption
BFC 的應用
防止 margin 重疊 (同一個BFC內的兩個兩個相鄰Box的 margin 會發生重疊,觸發生成兩個BFC,即不會重疊)
清除內部浮動 (創建一個新的 BFC,因為根據 BFC 的規則,計算 BFC 的高度時,浮動元素也參與計算)
自適應多欄布局 (BFC的區域不會與float box重疊。因此,可以觸發生成一個新的BFC)
8. 異步加載JS腳本的方式有哪些?標簽中增加 async(html5) 或者 defer(html4) 屬性,腳本就會異步加載。
defer 和 async 的區別在于:
defer 要等到整個頁面在內存中正常渲染結束(DOM 結構完全生成,以及其他腳本執行完成),在window.onload 之前執行;
async 一旦下載完,渲染引擎就會中斷渲染,執行這個腳本以后,再繼續渲染。
如果有多個 defer 腳本,會按照它們在頁面出現的順序加載
多個 async 腳本不能保證加載順序
動態創建 script 標簽
動態創建的 script ,設置 src 并不會開始下載,而是要添加到文檔中,JS文件才會開始下載。
let script = document.createElement("script"); script.src = "XXX.js"; // 添加到html文件中才會開始下載 document.body.append(script);
XHR 異步加載JS
let xhr = new XMLHttpRequest(); xhr.open("get", "js/xxx.js",true); xhr.send(); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { eval(xhr.responseText); } }9. ES5有幾種方式可以實現繼承?分別有哪些優缺點?
ES5 有 6 種方式可以實現繼承,分別為:
原型鏈繼承的基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
function SuperType() { this.name = "Yvette"; this.colors = ["pink", "blue", "green"]; } SuperType.prototype.getName = function () { return this.name; } function SubType() { this.age = 22; } SubType.prototype = new SuperType(); SubType.prototype.getAge = function() { return this.age; } SubType.prototype.constructor = SubType; let instance1 = new SubType(); instance1.colors.push("yellow"); console.log(instance1.getName()); //"Yvette" console.log(instance1.colors);//[ "pink", "blue", "green", "yellow" ] let instance2 = new SubType(); console.log(instance2.colors);//[ "pink", "blue", "green", "yellow" ]
缺點:
通過原型來實現繼承時,原型會變成另一個類型的實例,原先的實例屬性變成了現在的原型屬性,該原型的引用類型屬性會被所有的實例共享。
在創建子類型的實例時,沒有辦法在不影響所有對象實例的情況下給超類型的構造函數中傳遞參數。
借用構造函數的技術,其基本思想為:
在子類型的構造函數中調用超類型構造函數。
function SuperType(name) { this.name = name; this.colors = ["pink", "blue", "green"]; } function SubType(name) { SuperType.call(this, name); } let instance1 = new SubType("Yvette"); instance1.colors.push("yellow"); console.log(instance1.colors);//["pink", "blue", "green", yellow] let instance2 = new SubType("Jack"); console.log(instance2.colors); //["pink", "blue", "green"]
優點:
可以向超類傳遞參數
解決了原型中包含引用類型值被所有實例共享的問題
缺點:
方法都在構造函數中定義,函數復用無從談起,另外超類型原型中定義的方法對于子類型而言都是不可見的。
組合繼承指的是將原型鏈和借用構造函數技術組合到一塊,從而發揮二者之長的一種繼承模式。基本思路:
使用原型鏈實現對原型屬性和方法的繼承,通過借用構造函數來實現對實例屬性的繼承,既通過在原型上定義方法來實現了函數復用,又保證了每個實例都有自己的屬性。
function SuperType(name) { this.name = name; this.colors = ["pink", "blue", "green"]; } SuperType.prototype.sayName = function () { console.log(this.name); } function SuberType(name, age) { SuperType.call(this, name); this.age = age; } SuberType.prototype = new SuperType(); SuberType.prototype.constructor = SuberType; SuberType.prototype.sayAge = function () { console.log(this.age); } let instance1 = new SuberType("Yvette", 20); instance1.colors.push("yellow"); console.log(instance1.colors); //[ "pink", "blue", "green", "yellow" ] instance1.sayName(); //Yvette let instance2 = new SuberType("Jack", 22); console.log(instance2.colors); //[ "pink", "blue", "green" ] instance2.sayName();//Jack
缺點:
無論什么情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。
優點:
可以向超類傳遞參數
每個實例都有自己的屬性
實現了函數復用
原型繼承的基本思想:
借助原型可以基于已有的對象創建新對象,同時還不必因此創建自定義類型。
function object(o) { function F() { } F.prototype = o; return new F(); }
在 object() 函數內部,先穿甲一個臨時性的構造函數,然后將傳入的對象作為這個構造函數的原型,最后返回了這個臨時類型的一個新實例,從本質上講,object() 對傳入的對象執行了一次淺拷貝。
ECMAScript5通過新增 Object.create()方法規范了原型式繼承。這個方法接收兩個參數:一個用作新對象原型的對象和(可選的)一個為新對象定義額外屬性的對象(可以覆蓋原型對象上的同名屬性),在傳入一個參數的情況下,Object.create() 和 object() 方法的行為相同。
var person = { name: "Yvette", hobbies: ["reading", "photography"] } var person1 = Object.create(person); person1.name = "Jack"; person1.hobbies.push("coding"); var person2 = Object.create(person); person2.name = "Echo"; person2.hobbies.push("running"); console.log(person.hobbies);//[ "reading", "photography", "coding", "running" ] console.log(person1.hobbies);//[ "reading", "photography", "coding", "running" ]
在沒有必要創建構造函數,僅讓一個對象與另一個對象保持相似的情況下,原型式繼承是可以勝任的。
缺點:
同原型鏈實現繼承一樣,包含引用類型值的屬性會被所有實例共享。
寄生式繼承是與原型式繼承緊密相關的一種思路。寄生式繼承的思路與寄生構造函數和工廠模式類似,即創建一個僅用于封裝繼承過程的函數,該函數在內部已某種方式來增強對象,最后再像真地是它做了所有工作一樣返回對象。
function createAnother(original) { var clone = object(original);//通過調用函數創建一個新對象 clone.sayHi = function () {//以某種方式增強這個對象 console.log("hi"); }; return clone;//返回這個對象 } var person = { name: "Yvette", hobbies: ["reading", "photography"] }; var person2 = createAnother(person); person2.sayHi(); //hi
基于 person 返回了一個新對象 -—— person2,新對象不僅具有 person 的所有屬性和方法,而且還有自己的 sayHi() 方法。在考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式。
缺點:
使用寄生式繼承來為對象添加函數,會由于不能做到函數復用而效率低下。
同原型鏈實現繼承一樣,包含引用類型值的屬性會被所有實例共享。
所謂寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法,基本思路:
不必為了指定子類型的原型而調用超類型的構造函數,我們需要的僅是超類型原型的一個副本,本質上就是使用寄生式繼承來繼承超類型的原型,然后再將結果指定給子類型的原型。寄生組合式繼承的基本模式如下所示:
function inheritPrototype(subType, superType) { var prototype = object(superType.prototype); //創建對象 prototype.constructor = subType;//增強對象 subType.prototype = prototype;//指定對象 }
第一步:創建超類型原型的一個副本
第二步:為創建的副本添加 constructor 屬性
第三步:將新創建的對象賦值給子類型的原型
至此,我們就可以通過調用 inheritPrototype 來替換為子類型原型賦值的語句:
function SuperType(name) { this.name = name; this.colors = ["pink", "blue", "green"]; } //...code function SuberType(name, age) { SuperType.call(this, name); this.age = age; } SuberType.prototype = new SuperType(); inheritPrototype(SuberType, SuperType); //...code
優點:
只調用了一次超類構造函數,效率更高。避免在SuberType.prototype上面創建不必要的、多余的屬性,與其同時,原型鏈還能保持不變。
因此寄生組合繼承是引用類型最理性的繼承范式。
10. 隱藏頁面中的某個元素的方法有哪些?隱藏類型
屏幕并不是唯一的輸出機制,比如說屏幕上看不見的元素(隱藏的元素),其中一些依然能夠被讀屏軟件閱讀出來(因為讀屏軟件依賴于可訪問性樹來闡述)。為了消除它們之間的歧義,我們將其歸為三大類:
完全隱藏:元素從渲染樹中消失,不占據空間。
視覺上的隱藏:屏幕中不可見,占據空間。
語義上的隱藏:讀屏軟件不可讀,但正常占據空。
完全隱藏
display: none;
HTML5 新增屬性,相當于 display: none
視覺上的隱藏
設置 posoition 為 absolute 或 fixed,通過設置 top、left 等值,將其移出可視區域。
position:absolute; left: -99999px;
設置 position 為 relative,通過設置 top、left 等值,將其移出可視區域。
position: relative; left: -99999px; height: 0
設置 margin 值,將其移出可視區域范圍(可視區域占位)。
margin-left: -99999px; height: 0;
縮放
transform: scale(0); height: 0;
移動 translateX, translateY
transform: translateX(-99999px); height: 0
旋轉 rotate
transform: rotateY(90deg);
寬高為0,字體大小為0:
height: 0; width: 0; font-size: 0;
寬高為0,超出隱藏:
height: 0; width: 0; overflow: hidden;
opacity: 0;
visibility: hidden;
position: relative; z-index: -999;
再設置一個層級較高的元素覆蓋在此元素上。
clip-path: polygon(0 0, 0 0, 0 0, 0 0);
語義上的隱藏
讀屏軟件不可讀,占據空間,可見。
11. let、const、var 的區別有哪些?
聲明方式 | 變量提升 | 暫時性死區 | 重復聲明 | 塊作用域有效 | 初始值 | 重新賦值 |
---|---|---|---|---|---|---|
var | 會 | 不存在 | 允許 | 不是 | 非必須 | 允許 |
let | 不會 | 存在 | 不允許 | 是 | 非必須 | 允許 |
const | 不會 | 存在 | 不允許 | 是 | 必須 | 不允許 |
1.let/const 定義的變量不會出現變量提升,而 var 定義的變量會提升。
2.相同作用域中,let 和 const 不允許重復聲明,var 允許重復聲明。
3.const 聲明變量時必須設置初始值
4.const 聲明一個只讀的常量,這個常量不可改變。
這里有一個非常重要的點即是:在JS中,復雜數據類型,存儲在棧中的是堆內存的地址,存在棧中的這個地址是不變的,但是存在堆中的值是可以變得。有沒有相當常量指針/指針常量~
const a = 20; const b = { age: 18, star: 500 }
一圖勝萬言,如下圖所示,不變的是棧內存中 a 存儲的 20,和 b 中存儲的 0x0012ff21(瞎編的一個數字)。而 {age: 18, star: 200} 是可變的。
12. 說一說你對JS執行上下文棧和作用域鏈的理解?在開始說明JS上下文棧和作用域之前,我們先說明下JS上下文以及作用域的概念。
JS執行上下文執行上下文就是當前 JavaScript 代碼被解析和執行時所在環境的抽象概念, JavaScript 中運行任何的代碼都是在執行上下文中運行。
執行上下文類型分為:
全局執行上下文
函數執行上下文
執行上下文創建過程中,需要做以下幾件事:
創建變量對象:首先初始化函數的參數arguments,提升函數聲明和變量聲明。
創建作用域鏈(Scope Chain):在執行期上下文的創建階段,作用域鏈是在變量對象之后創建的。
確定this的值,即 ResolveThisBinding
作用域作用域負責收集和維護由所有聲明的標識符(變量)組成的一系列查詢,并實施一套非常嚴格的規則,確定當前執行的代碼對這些標識符的訪問權限。—— 摘錄自《你不知道的JavaScript》(上卷)
作用域有兩種工作模型:詞法作用域和動態作用域,JS采用的是詞法作用域工作模型,詞法作用域意味著作用域是由書寫代碼時變量和函數聲明的位置決定的。(with 和 eval 能夠修改詞法作用域,但是不推薦使用,對此不做特別說明)
作用域分為:
全局作用域
函數作用域
塊級作用域
JS執行上下文棧(后面簡稱執行棧)執行棧,也叫做調用棧,具有 LIFO (后進先出) 結構,用于存儲在代碼執行期間創建的所有執行上下文。
規則如下:
首次運行JavaScript代碼的時候,會創建一個全局執行的上下文并Push到當前的執行棧中,每當發生函數調用,引擎都會為該函數創建一個新的函數執行上下文并Push當前執行棧的棧頂。
當棧頂的函數運行完成后,其對應的函數執行上下文將會從執行棧中Pop出,上下文的控制權將移動到當前執行棧的下一個執行上下文。
以一段代碼具體說明:
function fun3() { console.log("fun3") } function fun2() { fun3(); } function fun1() { fun2(); } fun1();
Global Execution Context (即全局執行上下文)首先入棧,過程如下:
偽代碼:
//全局執行上下文首先入棧 ECStack.push(globalContext); //執行fun1(); ECStack.push(作用域鏈functionContext); //fun1中又調用了fun2; ECStack.push( functionContext); //fun2中又調用了fun3; ECStack.push( functionContext); //fun3執行完畢 ECStack.pop(); //fun2執行完畢 ECStack.pop(); //fun1執行完畢 ECStack.pop(); //javascript繼續順序執行下面的代碼,但ECStack底部始終有一個 全局上下文(globalContext);
作用域鏈就是從當前作用域開始一層一層向上尋找某個變量,直到找到全局作用域還是沒找到,就宣布放棄。這種一層一層的關系,就是作用域鏈。
如:
var a = 10; function fn1() { var b = 20; console.log(fn2) function fn2() { a = 20 } return fn2; } fn1()();
fn2作用域鏈 = [fn2作用域, fn1作用域,全局作用域]
13. 防抖函數的作用是什么?請實現一個防抖函數防抖函數的作用
防抖函數的作用就是控制函數在一定時間內的執行次數。防抖意味著N秒內函數只會被執行一次,如果N秒內再次被觸發,則重新計算延遲時間。
舉例說明: 小思最近在減肥,但是她非常吃吃零食。為此,與其男朋友約定好,如果10天不吃零食,就可以購買一個包(不要問為什么是包,因為包治百病)。但是如果中間吃了一次零食,那么就要重新計算時間,直到小思堅持10天沒有吃零食,才能購買一個包。所以,管不住嘴的小思,沒有機會買包(悲傷的故事)... 這就是 防抖。
防抖函數實現
事件第一次觸發時,timer 是 null,調用 later(),若 immediate 為true,那么立即調用 func.apply(this, params);如果 immediate 為 false,那么過 wait 之后,調用 func.apply(this, params)
事件第二次觸發時,如果 timer 已經重置為 null(即 setTimeout 的倒計時結束),那么流程與第一次觸發時一樣,若 timer 不為 null(即 setTimeout 的倒計時未結束),那么清空定時器,重新開始計時。
function debounce(func, wait, immediate = true) { let timeout, result; // 延遲執行函數 const later = (context, args) => setTimeout(() => { timeout = null;// 倒計時結束 if (!immediate) { //執行回調 result = func.apply(context, args); context = args = null; } }, wait); let debounced = function (...params) { if (!timeout) { timeout = later(this, params); if (immediate) { //立即執行 result = func.apply(this, params); } } else { clearTimeout(timeout); //函數在每個等待時延的結束被調用 timeout = later(this, params); } return result; } //提供在外部清空定時器的方法 debounced.cancel = function () { clearTimeout(timer); timer = null; }; return debounced; };
immediate 為 true 時,表示函數在每個等待時延的開始被調用。immediate 為 false 時,表示函數在每個等待時延的結束被調用。
防抖的應用場景
搜索框輸入查詢,如果用戶一直在輸入中,沒有必要不停地調用去請求服務端接口,等用戶停止輸入的時候,再調用,設置一個合適的時間間隔,有效減輕服務端壓力。
表單驗證
按鈕提交事件。
瀏覽器窗口縮放,resize事件(如窗口停止改變大小之后重新計算布局)等。
14. 節流函數的作用是什么?有哪些應用場景,請實現一個節流函數節流函數的作用
節流函數的作用是規定一個單位時間,在這個單位時間內最多只能觸發一次函數執行,如果這個單位時間內多次觸發函數,只能有一次生效。
節流函數實現
function throttle(func, wait, options = {}) { var timeout, context, args, result; var previous = 0; var later = function () { previous = options.leading === false ? 0 : (Date.now() || new Date().getTime()); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; var throttled = function () { var now = Date.now() || new Date().getTime(); if (!previous && options.leading === false) previous = now; //remaining 為距離下次執行 func 的時間 //remaining > wait,表示客戶端系統時間被調整過 var remaining = wait - (now - previous); context = this; args = arguments; //remaining 小于等于0,表示事件觸發的間隔時間大于設置的 wait if (remaining <= 0 || remaining > wait) { if (timeout) { //清空定時器 clearTimeout(timeout); timeout = null; } //重置 previous previous = now; //執行函數 result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; throttled.cancel = function () { clearTimeout(timeout); previous = 0; timeout = context = args = null; }; return throttled; }
禁用第一次首先執行,傳遞 {leading: false} ;想禁用最后一次執行,傳遞 {trailing: false}
節流的應用場景
按鈕點擊事件
拖拽事件
onScoll
計算鼠標移動的距離(mousemove)
15. 什么是閉包?閉包的作用是什么?《JavaScript高級程序設計》:
閉包是指有權訪問另一個函數作用域中的變量的函數
《JavaScript權威指南》:
從技術的角度講,所有的JavaScript函數都是閉包:它們都是對象,它們都關聯到作用域鏈。
《你不知道的JavaScript》
當函數可以記住并訪問所在的詞法作用域時,就產生了閉包,即使函數是在當前詞法作用域之外執行。
function foo() { var a = 2; return function fn() { console.log(a); } } let func = foo(); func(); //輸出2
閉包使得函數可以繼續訪問定義時的詞法作用域。拜 fn 所賜,在 foo() 執行后,foo 內部作用域不會被銷毀。
能夠訪問函數定義時所在的詞法作用域(阻止其被回收)。
私有化變量
function base() { let x = 10; //私有變量 return { getX: function() { return x; } } } let obj = base(); console.log(obj.getX()); //10
模擬塊級作用域
var a = []; for (var i = 0; i < 10; i++) { a[i] = (function(j){ return function () { console.log(j); } })(i); } a[6](); // 6
創建模塊
function coolModule() { let name = "Yvette"; let age = 20; function sayName() { console.log(name); } function sayAge() { console.log(age); } return { sayName, sayAge } } let info = coolModule(); info.sayName(); //"Yvette"
模塊模式具有兩個必備的條件(來自《你不知道的JavaScript》)
必須有外部的封閉函數,該函數必須至少被調用一次(每次調用都會創建一個新的模塊實例)
封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有作用域中形成閉包,并且可以訪問或者修改私有的狀態。
16. 實現 Promise.all 方法在實現 Promise.all 方法之前,我們首先要知道 Promise.all 的功能和特點,因為在清楚了 Promise.all 功能和特點的情況下,我們才能進一步去寫實現。
Promise.all 功能
Promise.all(iterable) 返回一個新的 Promise 實例。此實例在 iterable 參數內所有的 promise 都 fulfilled 或者參數中不包含 promise 時,狀態變成 fulfilled;如果參數中 promise 有一個失敗rejected,此實例回調失敗,失敗原因的是第一個失敗 promise 的返回結果。
let p = Promise.all([p1, p2, p3]);
p的狀態由 p1,p2,p3決定,分成以下;兩種情況:
(1)只有p1、p2、p3的狀態都變成 fulfilled,p的狀態才會變成 fulfilled,此時p1、p2、p3的返回值組成一個數組,傳遞給p的回調函數。
(2)只要p1、p2、p3之中有一個被 rejected,p的狀態就變成 rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。
Promise.all 的特點
Promise.all 的返回值是一個 promise 實例
如果傳入的參數為空的可迭代對象,Promise.all 會 同步 返回一個已完成狀態的 promise
如果傳入的參數中不包含任何 promise,Promise.all 會 異步 返回一個已完成狀態的 promise
其它情況下,Promise.all 返回一個 處理中(pending) 狀態的 promise.
Promise.all 返回的 promise 的狀態
如果傳入的參數中的 promise 都變成完成狀態,Promise.all 返回的 promise 異步地變為完成。
如果傳入的參數中,有一個 promise 失敗,Promise.all 異步地將失敗的那個結果給失敗狀態的回調函數,而不管其它 promise 是否完成
在任何情況下,Promise.all 返回的 promise 的完成狀態的結果都是一個數組
Promise.all 實現
Promise.all = function (promises) { //promises 是可迭代對象,省略參數合法性檢查 return new Promise((resolve, reject) => { //Array.from 將可迭代對象轉換成數組 promises = Array.from(promises); if (promises.length === 0) { resolve([]); } else { let result = []; let index = 0; for (let i = 0; i < promises.length; i++ ) { //考慮到 i 可能是 thenable 對象也可能是普通值 Promise.resolve(promises[i]).then(data => { result[i] = data; if (++index === promises.length) { //所有的 promises 狀態都是 fulfilled,promise.all返回的實例才變成 fulfilled 態 resolve(result); } }, err => { reject(err); return; }); } } }); }17. 請實現一個 flattenDeep 函數,把嵌套的數組扁平化
例如:
flattenDeep([1, [2, [3, [4]], 5]]); //[1, 2, 3, 4, 5]
利用 Array.prototype.flat
ES6 為數組實例新增了 flat 方法,用于將嵌套的數組“拉平”,變成一維的數組。該方法返回一個新數組,對原數組沒有影響。
flat 默認只會 “拉平” 一層,如果想要 “拉平” 多層的嵌套數組,需要給 flat 傳遞一個整數,表示想要拉平的層數。
function flattenDeep(arr, deepLength) { return arr.flat(deepLength); } console.log(flattenDeep([1, [2, [3, [4]], 5]], 3));
當傳遞的整數大于數組嵌套的層數時,會將數組拉平為一維數組,JS能表示的最大數字為 Math.pow(2, 53) - 1,因此我們可以這樣定義 flattenDeep 函數
function flattenDeep(arr) { //當然,大多時候我們并不會有這么多層級的嵌套 return arr.flat(Math.pow(2,53) - 1); } console.log(flattenDeep([1, [2, [3, [4]], 5]]));
利用 reduce 和 concat
function flattenDeep(arr){ return arr.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []); } console.log(flattenDeep([1, [2, [3, [4]], 5]]));
使用 stack 無限反嵌套多層嵌套數組
function flattenDeep(input) { const stack = [...input]; const res = []; while (stack.length) { // 使用 pop 從 stack 中取出并移除值 const next = stack.pop(); if (Array.isArray(next)) { // 使用 push 送回內層數組中的元素,不會改動原始輸入 original input stack.push(...next); } else { res.push(next); } } // 使用 reverse 恢復原數組的順序 return res.reverse(); } console.log(flattenDeep([1, [2, [3, [4]], 5]]));18. 請實現一個 uniq 函數,實現數組去重
例如:
uniq([1, 2, 3, 5, 3, 2]);//[1, 2, 3, 5]
法1: 利用ES6新增數據類型 Set
Set類似于數組,但是成員的值都是唯一的,沒有重復的值。
function uniq(arry) { return [...new Set(arry)]; }
法2: 利用 indexOf
function uniq(arry) { var result = []; for (var i = 0; i < arry.length; i++) { if (result.indexOf(arry[i]) === -1) { //如 result 中沒有 arry[i],則添加到數組中 result.push(arry[i]) } } return result; }
法3: 利用 includes
function uniq(arry) { var result = []; for (var i = 0; i < arry.length; i++) { if (!result.includes(arry[i])) { //如 result 中沒有 arry[i],則添加到數組中 result.push(arry[i]) } } return result; }
法4:利用 reduce
function uniq(arry) { return arry.reduce((prev, cur) => prev.includes(cur) ? prev : [...prev, cur], []); }
法5:利用 Map
function uniq(arry) { let map = new Map(); let result = new Array(); for (let i = 0; i < arry.length; i++) { if (map.has(arry[i])) { map.set(arry[i], true); } else { map.set(arry[i], false); result.push(arry[i]); } } return result; }19. 可迭代對象有哪些特點
ES6 規定,默認的 Iterator 接口部署在數據結構的 Symbol.iterator 屬性,換個角度,也可以認為,一個數據結構只要具有 Symbol.iterator 屬性(Symbol.iterator 方法對應的是遍歷器生成函數,返回的是一個遍歷器對象),那么就可以其認為是可迭代的。
可迭代對象的特點具有 Symbol.iterator 屬性,Symbol.iterator() 返回的是一個遍歷器對象
可以使用 for ... of 進行循環
通過被 Array.from 轉換為數組
let arry = [1, 2, 3, 4]; let iter = arry[Symbol.iterator](); console.log(iter.next()); //{ value: 1, done: false } console.log(iter.next()); //{ value: 2, done: false } console.log(iter.next()); //{ value: 3, done: false }原生具有 Iterator 接口的數據結構:
Array
Map
Set
String
TypedArray
函數的 arguments 對象
NodeList 對象
20. JSONP 的原理是什么?盡管瀏覽器有同源策略,但是 標簽的 src 屬性不會被同源策略所約束,可以獲取任意服務器上的腳本并執行。jsonp 通過插入 script 標簽的方式來實現跨域,參數只能通過 url 傳入,僅能支持 get 請求。
實現原理:
Step1: 創建 callback 方法
Step2: 插入 script 標簽
Step3: 后臺接受到請求,解析前端傳過去的 callback 方法,返回該方法的調用,并且數據作為參數傳入該方法
Step4: 前端執行服務端返回的方法調用
jsonp源碼實現
function jsonp({url, params, callback}) { return new Promise((resolve, reject) => { //創建script標簽 let script = document.createElement("script"); //將回調函數掛在 window 上 window[callback] = function(data) { resolve(data); //代碼執行后,刪除插入的script標簽 document.body.removeChild(script); } //回調函數加在請求地址上 params = {...params, callback} //wb=b&callback=show let arrs = []; for(let key in params) { arrs.push(`${key}=${params[key]}`); } script.src = `${url}?${arrs.join("&")}`; document.body.appendChild(script); }); }
使用:
function show(data) { console.log(data); } jsonp({ url: "http://localhost:3000/show", params: { //code }, callback: "show" }).then(data => { console.log(data); });
服務端代碼(node):
//express啟動一個后臺服務 let express = require("express"); let app = express(); app.get("/show", (req, res) => { let {callback} = req.query; //獲取傳來的callback函數名,callback是key res.send(`${callback}("Hello!")`); }); app.listen(3000);參考文章:
[1] 珠峰架構課(墻裂推薦)
[2] [JavaScript高級程序設計第六章]
[3] Step-By-Step】高頻面試題深入解析 / 周刊01
[4] Step-By-Step】高頻面試題深入解析 / 周刊02
[5] Step-By-Step】高頻面試題深入解析 / 周刊03
[6] Step-By-Step】高頻面試題深入解析 / 周刊04
謝謝各位小伙伴愿意花費寶貴的時間閱讀本文,如果本文給了您一點幫助或者是啟發,請不要吝嗇你的贊和Star,您的肯定是我前進的最大動力。 https://github.com/YvetteLau/...
關注公眾號,加入技術交流群。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/105008.html
摘要:獲取的對象范圍方法獲取的是最終應用在元素上的所有屬性對象即使沒有代碼,也會把默認的祖宗八代都顯示出來而只能獲取元素屬性中的樣式。因此對于一個光禿禿的元素,方法返回對象中屬性值如果有就是據我測試不同環境結果可能有差異而就是。 花了很長時間整理的前端面試資源,喜歡請大家不要吝嗇star~ 別只收藏,點個贊,點個star再走哈~ 持續更新中……,可以關注下github 項目地址 https:...
摘要:整理收藏一些優秀的文章及大佬博客留著慢慢學習原文協作規范中文技術文檔協作規范阮一峰編程風格凹凸實驗室前端代碼規范風格指南這一次,徹底弄懂執行機制一次弄懂徹底解決此類面試問題瀏覽器與的事件循環有何區別筆試題事件循環機制異步編程理解的異步 better-learning 整理收藏一些優秀的文章及大佬博客留著慢慢學習 原文:https://www.ahwgs.cn/youxiuwenzhan...
摘要:寫在前面的話最近互聯網朋友圈充斥著一股恐慌的氣息。本人作為一名,萬不敢稱資深,只是呆過幾年大型央企和大型互聯網企業,聊有一點自己的看法罷了。如果不放心,以一周為期,對展示在面前的機會進行初步分級。也可以略高于期望,以此探一探對方的反應。 showImg(https://segmentfault.com/img/bVblxeY?w=1008&h=298); 寫在前面的話 最近互聯網朋...
摘要:終于,我在看到美團的社招信息后,勇敢地邁出了第一步。當時參加的是美團點評部門的面試,部門前端技術棧是,后端用的。后來才知道美團是一次性全部面完的。所以以后有去參加美團面試的童鞋,最好做好面試四個小時的打算。 showImg(https://segmentfault.com/img/bV0c3T?w=672&h=361); 前言 我叫王小閏(花名),非科班出身,野生前端從業者,在小公司打...
閱讀 1209·2021-11-17 09:33
閱讀 3617·2021-09-28 09:42
閱讀 3345·2021-09-13 10:35
閱讀 2504·2021-09-06 15:00
閱讀 2450·2021-08-27 13:12
閱讀 3617·2021-07-26 23:38
閱讀 1855·2019-08-30 15:55
閱讀 546·2019-08-30 15:53