超好運(yùn)的大廠實(shí)習(xí)(帶轉(zhuǎn)正)面試,直接拿下!猛猛的!
Hello,大家好,我是 Sunday。
運(yùn)氣也是實(shí)力的一部分,此話誠不欺我!
最近有位同學(xué),校招面試攜程,一面、二面,都是純八股文的面試。這可是 帶轉(zhuǎn)正的 HC 呀! 直接輕松拿下。
實(shí)習(xí)一天 220,額外加 1000/月 的房補(bǔ)。在大廠中算是中等水平。
那么下面咱們就來看看這次面試,都聊了什么問題吧!
一、improt和require的區(qū)別
import 和 require 是 JavaScript 中用來導(dǎo)入模塊的兩種不同的語法,主要用于 ES6 和 CommonJS 模塊系統(tǒng)。以下是它們的主要區(qū)別:
1. 模塊系統(tǒng)
- require: 屬于 CommonJS 模塊系統(tǒng),主要用于 Node.js 中。它是同步加載模塊的,適合在服務(wù)器端使用。
- import: 屬于 ES6 模塊系統(tǒng)(也稱為 ECMAScript 模塊)。它支持異步加載,適合在瀏覽器端和現(xiàn)代 JavaScript 環(huán)境中使用。
2. 語法
- require:
const module = require('module-name');
- import:
import module from 'module-name'; // 導(dǎo)入默認(rèn)導(dǎo)出
import { namedExport } from 'module-name'; // 導(dǎo)入命名導(dǎo)出
import * as allExports from 'module-name'; // 導(dǎo)入所有導(dǎo)出
3. 加載時(shí)機(jī)
- require: 在代碼運(yùn)行時(shí)被調(diào)用,模塊在被調(diào)用時(shí)才會(huì)被加載,因此是同步的。
- import: 在編譯階段被處理,模塊會(huì)在模塊代碼執(zhí)行前被加載,因此是異步的。
4. Hoisting(提升)
- require: 不會(huì)提升,必須在代碼中顯式調(diào)用。
- import: 會(huì)提升,可以在文件的任何位置使用,但在模塊的頂部進(jìn)行導(dǎo)入是最佳實(shí)踐。
5. 導(dǎo)出方式
CommonJS 導(dǎo)出:
// 導(dǎo)出模塊
module.exports = {
functionA: function() {},
functionB: function() {},
};
ES6 導(dǎo)出:
// 導(dǎo)出默認(rèn)
export default functionA;
// 導(dǎo)出命名
export function functionB() {}
6. 循環(huán)依賴處理
- require: 處理循環(huán)依賴時(shí),返回的是已經(jīng)加載的模塊部分。
- import: 在循環(huán)依賴的情況下,會(huì)引發(fā)錯(cuò)誤,因?yàn)樗谀K解析階段就會(huì)出錯(cuò)。
7. 總結(jié)
- require: 適合 Node.js 和后端開發(fā),采用同步加載的方式。
- import: 更加現(xiàn)代,適合前端開發(fā),支持異步加載和靜態(tài)分析。
在現(xiàn)代前端開發(fā)中,建議使用 ES6 的 import 語法,因?yàn)樗峁┝烁玫哪K化和可維護(hù)性。
二、require 有辦法實(shí)現(xiàn)異步嗎?
在 Node.js 中,require 本身是同步的,但可以通過一些其他方式實(shí)現(xiàn)異步模塊加載。以下是幾種方法:
1. 使用 import()
從 ES6 開始,可以使用動(dòng)態(tài) import() 函數(shù)來實(shí)現(xiàn)異步加載模塊。雖然它與 require 不同,但在現(xiàn)代 JavaScript 環(huán)境中是推薦的方式。
async function loadModule() {
const module = await import('./module.js');
module.someFunction();
}
2. 使用 require 結(jié)合回調(diào)
可以在回調(diào)中使用 require,盡管這并不是真正的異步加載,但可以通過事件循環(huán)達(dá)到異步效果。
function loadModule(callback) {
setTimeout(() => {
const module = require('./module.js');
callback(module);
}, 0);
}
loadModule((module) => {
module.someFunction();
});
3. 使用 require 結(jié)合 Promise
通過 Promise 的方式包裹 require,達(dá)到異步的效果:
function loadModule() {
return new Promise((resolve) => {
setTimeout(() => {
const module = require('./module.js');
resolve(module);
}, 0);
});
}
loadModule().then((module) => {
module.someFunction();
});
4. 使用 async_hooks
在更復(fù)雜的場(chǎng)景中,可以使用 Node.js 的 async_hooks 模塊來處理異步上下文,但這通常不適用于簡(jiǎn)單的模塊加載需求。
5. 總結(jié)
雖然 require 本身是同步的,不能直接實(shí)現(xiàn)異步模塊加載,但可以使用動(dòng)態(tài) import() 或通過回調(diào)和 Promise 來模擬異步行為。在現(xiàn)代開發(fā)中,推薦使用 ES6 的動(dòng)態(tài) import(),因?yàn)樗呖勺x性和一致性。
三、export 和 export default的區(qū)別
export 和 export default 是 ES6 中用于導(dǎo)出模塊的兩種方式,它們的主要區(qū)別如下:
1. 導(dǎo)出方式
(1) export
// module.js
export const a = 1;
export const b = 2;
export function add(x, y) {
return x + y;
}
// main.js
import { a, add } from './module.js';
console.log(a); // 1
console.log(add(1, 2)); // 3
允許在一個(gè)模塊中導(dǎo)出多個(gè)變量、函數(shù)或類。
導(dǎo)出時(shí)不需要指定名稱,可以在導(dǎo)入時(shí)使用 {} 指定要導(dǎo)入的變量名稱。
(2) export default
// module.js
const multiply = (x, y) => x * y;
export default multiply;
// main.js
import multiply from './module.js';
console.log(multiply(2, 3)); // 6
每個(gè)模塊只能有一個(gè)默認(rèn)導(dǎo)出。
在導(dǎo)入時(shí)可以使用任何名稱,不需要使用 {}。
2. 導(dǎo)入方式
使用export的導(dǎo)入:
import { a, b } from './module.js';
導(dǎo)入時(shí)需要使用相同的名稱,且需要使用 {} 包裹。
使用export default的導(dǎo)入:
import myFunction from './module.js';
導(dǎo)入時(shí)可以自定義名稱,且不需要 {}。
3. 使用場(chǎng)景
- export: 當(dāng)你需要導(dǎo)出多個(gè)功能或變量時(shí),適合使用 export。
- export default: 當(dāng)一個(gè)模塊有一個(gè)主要功能或?qū)ο髸r(shí),適合使用 export default。
4. 組合使用
可以同時(shí)使用兩者:
// module.js
export const a = 1;
export const b = 2;
const multiply = (x, y) => x * y;
export default multiply;
// main.js
import multiply, { a } from './module.js';
console.log(a); // 1
console.log(multiply(2, 3)); // 6
5. 總結(jié)
export 允許多個(gè)命名導(dǎo)出,而 export default 只允許一個(gè)默認(rèn)導(dǎo)出。
導(dǎo)入時(shí),命名導(dǎo)出需要使用 {},而默認(rèn)導(dǎo)入可以使用任意名稱。
四、閉包
能夠調(diào)用其他函數(shù)中變量的函數(shù),就是 閉包函數(shù)。
function outerFunction() {
let outerVariable = 'I am outside!';
// innerFunction 函數(shù)使用了 outerFunction 里面的變量,所以它就是【閉包函數(shù)】
function innerFunction() {
console.log(outerVariable);
}
return innerFunction;
}
const closureFunction = outerFunction(); // outerFunction 被執(zhí)行
closureFunction(); // 輸出: I am outside!
五、在什么情況下閉包會(huì)出現(xiàn)內(nèi)存泄漏的問題
閉包在某些情況下會(huì)導(dǎo)致內(nèi)存泄漏,主要是由于閉包持有外部變量的引用,進(jìn)而阻止了這些變量的垃圾回收。以下是幾種可能導(dǎo)致內(nèi)存泄漏的情況:
1. 循環(huán)引用
當(dāng)閉包持有外部對(duì)象的引用,而該對(duì)象又持有對(duì)閉包的引用時(shí),就會(huì)產(chǎn)生循環(huán)引用。這種情況會(huì)導(dǎo)致垃圾回收器無法正確回收內(nèi)存。
function createClosure() {
let obj = {};
obj.closure = function() {
console.log(obj);
};
return obj.closure;
}
const closure = createClosure(); // obj 和 closure 形成循環(huán)引用
在上面的例子中,obj 對(duì)象持有對(duì)閉包的引用,而閉包又引用了 obj,這導(dǎo)致內(nèi)存無法被回收。
2. 不當(dāng)使用事件監(jiān)聽器
如果使用閉包作為事件監(jiān)聽器并沒有在適當(dāng)?shù)臅r(shí)候移除它們,可能會(huì)導(dǎo)致內(nèi)存泄漏。特別是在動(dòng)態(tài)創(chuàng)建和銷毀 DOM 元素時(shí)。
function createElement() {
const button = document.createElement('button');
button.innerText = 'Click me';
button.addEventListener('click', function() {
// 使用了外部變量
console.log('Button clicked');
});
document.body.appendChild(button);
}
// 每次調(diào)用 createElement 都會(huì)添加一個(gè)新的按鈕,但事件監(jiān)聽器不會(huì)被移除
createElement();
在這個(gè)例子中,每次調(diào)用 createElement 函數(shù)都會(huì)創(chuàng)建一個(gè)新的按鈕并添加事件監(jiān)聽器,但如果不手動(dòng)移除這些監(jiān)聽器,它們將一直保留在內(nèi)存中。
3. 大型數(shù)據(jù)結(jié)構(gòu)
當(dāng)閉包持有大型數(shù)據(jù)結(jié)構(gòu)(如大數(shù)組或?qū)ο螅┑囊脮r(shí),如果這些數(shù)據(jù)結(jié)構(gòu)沒有在不需要時(shí)被清理,也可能導(dǎo)致內(nèi)存泄漏。
let largeArray = new Array(1000000).fill('data');
function createLargeDataClosure() {
return function() {
console.log(largeArray.length); // 持有對(duì) largeArray 的引用
};
}
const closure = createLargeDataClosure();
在這個(gè)例子中,closure 持有對(duì) largeArray 的引用,如果沒有在適當(dāng)?shù)臅r(shí)候釋放這個(gè)引用,將會(huì)導(dǎo)致內(nèi)存占用持續(xù)增加。
4. 使用全局作用域
如果閉包中的變量是全局變量,那么閉包會(huì)一直持有對(duì)這些全局變量的引用,直到整個(gè)程序結(jié)束。這在長時(shí)間運(yùn)行的應(yīng)用中,可能導(dǎo)致內(nèi)存的不斷增長。
let globalVar = 'Hello';
function createClosure() {
return function() {
console.log(globalVar);
};
}
const closure = createClosure();
在這個(gè)例子中,閉包始終引用了 globalVar,從而保持了對(duì)它的引用。
5. 預(yù)防內(nèi)存泄漏的措施
- 合理使用閉包:盡量避免不必要的閉包,尤其是在循環(huán)和事件處理器中。
- 及時(shí)清理引用:在不再需要的時(shí)候,主動(dòng)清理閉包中的引用,例如通過將事件監(jiān)聽器設(shè)置為 null。
- 使用 WeakMap 和 WeakSet:對(duì)于需要被垃圾回收的對(duì)象引用,可以使用 WeakMap 和 WeakSet,它們不會(huì)阻止垃圾回收。
- 手動(dòng)移除事件監(jiān)聽器:在元素被移除時(shí),記得手動(dòng)移除事件監(jiān)聽器。
通過以上方式,可以有效減少使用閉包時(shí)可能出現(xiàn)的內(nèi)存泄漏問題。
六、說一下 JS 中原型鏈的概念
原型鏈?zhǔn)?JavaScript 中實(shí)現(xiàn)繼承的一種機(jī)制,每個(gè)對(duì)象都有一個(gè)內(nèi)部屬性指向其原型對(duì)象。原型鏈?zhǔn)峭ㄟ^對(duì)象的 __proto__ 屬性或 Object.getPrototypeOf() 方法連接起來的。下面是原型鏈的基本概念以及圖示說明。
1. 原型鏈的概念
- 原型:每個(gè) JavaScript 對(duì)象都有一個(gè)原型,原型也是一個(gè)對(duì)象,可以有自己的原型,這樣就形成了一個(gè)鏈?zhǔn)浇Y(jié)構(gòu),稱為原型鏈。
- 構(gòu)造函數(shù):通過構(gòu)造函數(shù)創(chuàng)建的對(duì)象會(huì)有一個(gè) prototype 屬性,指向構(gòu)造函數(shù)的原型對(duì)象。
查找順序:
- 當(dāng)訪問一個(gè)對(duì)象的屬性時(shí),JavaScript 引擎首先在該對(duì)象自身查找。如果找不到,它會(huì)查找該對(duì)象的原型(即 __proto__ 屬性指向的對(duì)象)。
- 如果在原型中也沒有找到,它會(huì)繼續(xù)向上查找,直到找到屬性或到達(dá)原型鏈的末端(通常是 Object.prototype),如果仍未找到,則返回 undefined。
2. 示例代碼
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(this.name + ' makes a noise.');
};
function Dog(name) {
Animal.call(this, name); // Call the parent constructor
}
// 設(shè)置 Dog.prototype 為 Animal 的實(shí)例
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 糾正 constructor
Dog.prototype.speak = function() {
console.log(this.name + ' barks.');
};
const dog = new Dog('Rex');
dog.speak(); // 輸出: Rex barks.
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
3. 解析示例
Animal 是一個(gè)構(gòu)造函數(shù),創(chuàng)建的實(shí)例具有 name 屬性和 speak 方法。
Dog 是另一個(gè)構(gòu)造函數(shù),它通過 Object.create(Animal.prototype) 使得 Dog.prototype 成為 Animal.prototype 的實(shí)例,從而繼承了 Animal 的方法。
dog 是 Dog 的一個(gè)實(shí)例,它可以訪問 speak 方法,雖然該方法在 Dog 的原型上被覆蓋,但它也可以訪問 Animal 的 speak 方法(如果需要的話)。
4. 小結(jié)
原型鏈?zhǔn)?JavaScript 繼承的基礎(chǔ),允許對(duì)象共享屬性和方法。
理解原型鏈對(duì)于掌握 JavaScript 的面向?qū)ο缶幊?、?gòu)造函數(shù)、以及如何實(shí)現(xiàn)繼承非常重要。
七、null有原型鏈嗎?
null 在 JavaScript 中被認(rèn)為是一個(gè)特殊的原始值。它表示“無”或“空值”。在原型鏈的上下文中,null 是原型鏈的終點(diǎn)。
八、Array的原型是什么
在 JavaScript 中,Array 是一種內(nèi)置的對(duì)象,用于處理數(shù)組。Array 的原型是 Array.prototype。這使得所有數(shù)組實(shí)例都能夠訪問 Array.prototype 上定義的方法和屬性。
1. 原型鏈結(jié)構(gòu)
(1) Array.prototype:
- Array.prototype 是所有數(shù)組對(duì)象的原型,定義了數(shù)組的方法和屬性,如 push(), pop(), map(), filter() 等。
- 你可以在 Array.prototype 上添加自定義的方法,這樣所有數(shù)組實(shí)例都可以使用。
(2) Object.prototype:
- 所有對(duì)象(包括數(shù)組)的原型最終都會(huì)鏈接到 Object.prototype。這意味著數(shù)組實(shí)例也繼承了 Object 的方法和屬性,如 toString(), hasOwnProperty() 等。
(3) null:
- Object.prototype 的原型是 null,這是原型鏈的終點(diǎn)。
2. 示例
下面是一個(gè)示例,展示了如何通過 Array.prototype 訪問數(shù)組方法:
// 定義一個(gè)數(shù)組
const arr = [1, 2, 3];
// 訪問 Array.prototype 上的方法
console.log(arr.map(x => x * 2)); // 輸出: [2, 4, 6]
// 檢查原型
console.log(Object.getPrototypeOf(arr) === Array.prototype); // 輸出: true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // 輸出: true
3. 總結(jié)
Array 的原型是 Array.prototype。
所有數(shù)組實(shí)例都可以訪問 Array.prototype 上定義的方法和屬性。
Array.prototype 又可以通過其原型鏈訪問到 Object.prototype,最終終止于 null。
九、Object的原型是什么
在 JavaScript 中,Object 是所有對(duì)象的基類。Object 的原型是 Object.prototype。這是一個(gè)包含許多通用方法和屬性的對(duì)象,所有普通對(duì)象都可以繼承這些方法和屬性。
1. 原型鏈結(jié)構(gòu)
(1) Object.prototype:
- Object.prototype 是所有對(duì)象的原型,它定義了一些通用的方法和屬性,例如 toString(), hasOwnProperty(), isPrototypeOf(), 和 valueOf() 等。
- 所有普通對(duì)象(即通過對(duì)象字面量、構(gòu)造函數(shù)或 Object.create() 創(chuàng)建的對(duì)象)都能夠訪問 Object.prototype 上的方法和屬性。
(2) null:
Object.prototype 的原型是 null,這是原型鏈的終點(diǎn)。
2. 示例
下面是一個(gè)示例,展示了如何通過 Object.prototype 訪問對(duì)象方法:
// 創(chuàng)建一個(gè)普通對(duì)象
const obj = {
name: 'Alice',
age: 30
};
// 訪問 Object.prototype 上的方法
console.log(obj.toString()); // 輸出: [object Object]
console.log(obj.hasOwnProperty('name')); // 輸出: true
// 檢查原型
console.log(Object.getPrototypeOf(obj) === Object.prototype); // 輸出: true
console.log(Object.getPrototypeOf(Object.prototype)); // 輸出: null
3. 總結(jié)
Object 的原型是 Object.prototype。
所有普通對(duì)象都可以訪問 Object.prototype 上定義的方法和屬性。
Object.prototype 的原型是 null,這是原型鏈的終點(diǎn)。
十、let const var的區(qū)別
let、const 和 var 是 JavaScript 中用于聲明變量的關(guān)鍵字。它們之間有一些重要的區(qū)別:
1. 聲明的作用域
(1) var:
聲明的變量具有 函數(shù)作用域。如果在函數(shù)內(nèi)部使用 var 聲明變量,該變量只在該函數(shù)內(nèi)可用。如果在函數(shù)外部使用 var 聲明,變量會(huì)變成全局變量。
例子:
function example() {
var a = 10;
}
console.log(a); // ReferenceError: a is not defined
(2) let和const:
聲明的變量具有 塊級(jí)作用域。也就是說,它們只在其聲明的代碼塊內(nèi)有效(例如,if 語句、for 循環(huán)等)。
例子:
{
let b = 20;
const c = 30;
}
console.log(b); // ReferenceError: b is not defined
console.log(c); // ReferenceError: c is not defined
2. 變量提升
(1) var:
變量會(huì)被提升到函數(shù)的頂部,初始化為 undefined。在變量聲明之前使用該變量不會(huì)報(bào)錯(cuò),但值為 undefined。
例子:
console.log(d); // undefined
var d = 5;
console.log(d); // 5
(2) let和const:
變量也會(huì)被提升,但在聲明之前是 “暫時(shí)性死區(qū)”(Temporal Dead Zone, TDZ),使用它們會(huì)導(dǎo)致 ReferenceError。
例子:
console.log(e); // ReferenceError: Cannot access 'e' before initialization
let e = 15;
3. 重新賦值
(1) var和let:
這兩個(gè)關(guān)鍵字聲明的變量都可以被重新賦值。
例子:
var f = 1;
f = 2; // 可以重新賦值
console.log(f); // 2
let g = 3;
g = 4; // 可以重新賦值
console.log(g); // 4
(2) const:
聲明的變量不能被重新賦值,必須在聲明時(shí)初始化。
例子:
const h = 5;
h = 6; // TypeError: Assignment to constant variable.
4. 常量和可變性
const:
用于聲明常量,但這只意味著變量的引用不可以被改變。如果 const 聲明的是一個(gè)對(duì)象或數(shù)組,可以修改其屬性或元素。
例子:
const obj = { name: 'Alice' };
obj.name = 'Bob'; // 允許,修改對(duì)象的屬性
console.log(obj.name); // Bob
5. 總結(jié)
- var: 函數(shù)作用域、提升、可以重復(fù)聲明和賦值。
- let: 塊級(jí)作用域、提升、可以賦值,但不能重復(fù)聲明。
- const: 塊級(jí)作用域、提升、不能重新賦值,聲明時(shí)必須初始化。
十一、const的原理是什么?為什么不能改變
const 是 JavaScript 中用于聲明常量的關(guān)鍵字,其原理和行為主要與塊級(jí)作用域和引用類型有關(guān)。以下是對(duì) const 的詳細(xì)解釋:
1. 塊級(jí)作用域
const 聲明的變量具有塊級(jí)作用域。這意味著它們只在定義它們的代碼塊內(nèi)有效。例如,在一個(gè) if 語句或循環(huán)內(nèi)聲明的 const 變量不能在其外部訪問。
{
const x = 10;
}
console.log(x); // ReferenceError: x is not defined
2. 不可重新賦值
const 聲明的變量不能被重新賦值。這并不是說變量本身是不可變的,而是說變量的引用(地址)是不可變的。也就是說,您不能將一個(gè) const 變量重新指向一個(gè)新的值。
const y = 20;
y = 30; // TypeError: Assignment to constant variable.
3. 引用類型的可變性
如果 const 聲明的是一個(gè)對(duì)象或數(shù)組,您可以修改其屬性或元素,但不能改變其引用。這意味著您可以對(duì)對(duì)象的內(nèi)容進(jìn)行修改,但不能將該對(duì)象指向另一個(gè)對(duì)象。
const obj = { name: 'Alice' };
obj.name = 'Bob'; // 允許,修改對(duì)象的屬性
console.log(obj.name); // Bob
obj = { name: 'Charlie' }; // TypeError: Assignment to constant variable.
4. 原理解析
const 的原理在于 ECMAScript 的變量綁定機(jī)制。聲明一個(gè) const 變量時(shí),它會(huì)創(chuàng)建一個(gè)綁定到該值的不可更改的引用。以下是這一機(jī)制的詳細(xì)解讀:
- 綁定:當(dāng)您使用 const 聲明一個(gè)變量時(shí),實(shí)際上是將該變量與一個(gè)值綁定在一起。這個(gè)綁定是固定的,您不能改變它。
- 作用域:const 聲明的變量?jī)H在聲明時(shí)的塊內(nèi)有效,并在退出該塊后被銷毀。
- 不可重新賦值:一旦綁定建立,就不能再將該變量指向另一個(gè)值(即不可重新賦值)。
5. 總結(jié)
const 聲明的是一個(gè)不可重新賦值的變量,但它的值(如果是對(duì)象或數(shù)組)可以是可變的。
這種設(shè)計(jì)使得 const 適用于需要保護(hù)不被重新分配的變量,同時(shí)仍允許對(duì)其內(nèi)部狀態(tài)進(jìn)行修改的場(chǎng)景。