70 个 JavaScript 面试题
一、undefined
与 null
的区别
在理解 undefined
和 null
之间的区别之前,我们首先要了解它们之间的相似之处。它们属于 JavaScript 的七种原始类型之一,并且都是“假值”,这意味着当它们被转换为布尔值时,结果为 false
。
相似之处
-
原始类型:JavaScript 中的七个原始类型包括字符串、数字、
null
、undefined
、布尔值、符号和大整数。let primitiveTypes = ['string', 'number', 'null', 'undefined', 'boolean', 'symbol', 'bigint'];
-
假值:当转换成布尔值时,它们会被评估为
false
。console.log(!!null); // 输出 false
console.log(!!undefined); // 输出 false
console.log(Boolean(null)); // 输出 false
console.log(Boolean(undefined)); // 输出 false
区别
-
**
undefined
**:这是变量未被赋予具体值时的默认值。或者是一个没有显式返回值的函数的结果,或者是在对象中不存在的属性。let _thisIsUndefined;
const doNothing = () => {};
const someObj = {
a: "ay",
b: "bee",
c: "si"
};
console.log(_thisIsUndefined); // 输出 undefined
console.log(doNothing()); // 输出 undefined
console.log(someObj["d"]); // 输出 undefined -
**
null
**:“代表没有值的值”。这是明确地赋给变量的一个值。在这个例子中,当读取文件的方法没有抛出错误时,我们得到的值是null
。fs.readFile('path/to/file', (e, data) => {
console.log(e); // 当没有错误发生时输出 null
if (e) {
console.log(e);
}
console.log(data);
});
当我们比较 null
和 undefined
时,使用 ==
操作符会得到 true
,而使用 ===
操作符会得到 false
。
console.log(null == undefined); // 输出 true
console.log(null === undefined); // 输出 false
二、逻辑与操作符 &&
的作用
逻辑与操作符 &&
寻找其操作数中的第一个假值表达式并返回该表达式;如果没有找到任何假值表达式,则返回最后一个表达式。它使用短路评估来避免不必要的工作。
console.log(false && 1 && []); // 输出 false
console.log(" " && true && 5); // 输出 5
在使用 if
语句时:
const router: Router = Router();
router.get('/endpoint', (req: Request, res: Response) => {
let conMobile: PoolConnection;
try {
// 执行一些数据库操作
} catch (e) {
if (conMobile) {
conMobile.release();
}
}
});
使用 &&
操作符简化代码:
const router: Router = Router();
router.get('/endpoint', (req: Request, res: Response) => {
let conMobile: PoolConnection;
try {
// 执行一些数据库操作
} catch (e) {
conMobile && conMobile.release();
}
});
三、逻辑或操作符 ||
的作用
逻辑或操作符 ||
寻找其操作数中的第一个真值表达式并返回该表达式。这也使用短路评估来避免不必要的工作。在支持 ES6 默认函数参数之前,这常用来初始化函数中的默认参数值。
console.log(null || 1 || undefined); // 输出 1
function logName(name) {
var n = name || "Mark";
console.log(n);
}
logName(); // 输出 "Mark"
四、使用 +
或单目加号运算符是否是将字符串转换为数字的最快方式?
根据 MDN 文档,+
运算符是将字符串转换为数字的最快方式,因为它不会对已经是数字的值执行任何操作。
五、什么是 DOM?
DOM(文档对象模型)是一种为 HTML 和 XML 文档提供接口的应用程序编程接口(API)。当浏览器首次读取(解析)我们的 HTML 文档时,它会创建一个基于 HTML 文档的大对象,这就是 DOM。它是从 HTML 文档建模出来的一种树状结构。DOM 用于交互和修改 DOM 结构或特定元素或节点。
假设我们有如下的 HTML 结构:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document Object Model</title>
</head>
<body>
<div>
<p>
<span></span>
</p>
<label></label>
<input>
</div>
</body>
</html>
其 DOM 等效结构如下所示:
DOM 等效结构
JavaScript 中的对象 document
代表了 DOM。它提供了许多我们可以用来选择元素、更新元素内容等的方法。
六、什么是事件传播?
当 DOM 元素上发生事件时,该事件不仅发生在单个元素上。在冒泡阶段,事件向上冒泡,直到到达 window
;而在捕获阶段,事件从 window
开始向下传播到触发事件的元素。
事件传播有三个阶段:
-
捕获阶段:事件从 window
开始向下传递到每个元素,直到达到目标元素。 -
目标阶段:事件已达到目标元素。 -
冒泡阶段:事件从目标元素开始向上冒泡,直到到达 window
。
七、什么是事件冒泡?
当一个 DOM 元素上发生事件时,事件不仅仅发生在该元素上。在冒泡阶段,事件会向上冒泡,直到到达 window
。
假设我们有这样的标记:
<div class="grandparent">
<div class="parent">
<div class="child">1</div>
</div>
</div>
对应的 JS 代码如下:
function addEvent(el, event, callback, isCapture = false) {
if (!el || !event || !callback || typeof callback !== 'function') return;
if (typeof el === 'string') {
el = document.querySelector(el);
};
el.addEventListener(event, callback, isCapture);
}
addEvent(document, 'DOMContentLoaded', () => {
const child = document.querySelector('.child');
const parent = document.querySelector('.parent');
const grandparent = document.querySelector('.grandparent');
addEvent(child, 'click', function (e) {
console.log('child');
});
addEvent(parent, 'click', function (e) {
console.log('parent');
});
addEvent(grandparent, 'click', function (e) {
console.log('grandparent');
});
addEvent(document, 'click', function (e) {
console.log('document');
});
addEvent('html', 'click', function (e) {
console.log('html');
})
addEvent(window, 'click', function (e) {
console.log('window');
})
});
addEventListener
方法有一个可选的第三个参数 useCapture
,默认值为 false
。如果设置为 true
,事件将在捕获阶段发生。如果我们点击 child
元素,控制台将依次记录 child
, parent
, grandparent
, html
, document
, window
。这就是事件冒泡。
八、什么是事件捕获?
当一个 DOM 元素上发生事件时,事件不仅仅发生在该元素上。在捕获阶段,事件从 window
向下传播到触发事件的元素。
假设我们有这样的标记:
<div class="grandparent">
<div class="parent">
<div class="child">1</div>
</div>
</div>
对应的 JS 代码如下:
function addEvent(el, event, callback, isCapture = false) {
if (!el || !event || !callback || typeof callback !== 'function') return;
if (typeof el === 'string') {
el = document.querySelector(el);
};
el.addEventListener(event, callback, isCapture);
}
addEvent(document, 'DOMContentLoaded', () => {
const child = document.querySelector('.child');
const parent = document.querySelector('.parent');
const grandparent = document.querySelector('.grandparent');
addEvent(child, 'click', function (e) {
console.log('child');
}, true);
addEvent(parent, 'click', function (e) {
console.log('parent');
}, true);
addEvent(grandparent, 'click', function (e) {
console.log('grandparent');
}, true);
addEvent(document, 'click', function (e) {
console.log('document');
}, true);
addEvent('html', 'click', function (e) {
console.log('html');
}, true)
addEvent(window, 'click', function (e) {
console.log('window');
}, true)
});
addEventListener
方法有一个可选的第三个参数 useCapture
,默认值为 false
。如果设置为 true
,事件将在捕获阶段发生。如果我们点击 child
元素,控制台将依次记录 window
, document
, html
, grandparent
, parent
, child
。这就是事件捕获。
九、event.preventDefault()
与 event.stopPropagation()
方法的区别
event.preventDefault()
方法阻止元素的默认行为。如果在一个 <form>
元素中使用,它会阻止表单提交;如果在一个 <a>
元素中使用,它会阻止导航;如果在一个上下文菜单 <menu>
元素中使用,它会阻止显示上下文菜单。而 event.stopPropagation()
方法则停止事件的传播,即阻止事件在冒泡或捕获阶段继续传播。
十、如何判断 event.preventDefault()
是否在一个元素中使用?
我们可以使用事件对象中的 event.defaultPrevented
属性。它返回一个布尔值,指示是否在一个特定元素上调用了 event.preventDefault()
方法。
十一、为什么这段代码会抛出错误? obj.someprop.x
const obj = {};
console.log(obj.someprop.x); // 抛出错误
显然,这里抛出错误是因为我们试图访问一个不存在的属性的子属性。记住,在对象中不存在的属性,默认值是 undefined
,而 undefined
是没有 x
属性的。
十二、event.target
是什么?
简单来说,event.target
是触发事件的元素。
示例 HTML 标记:
<div onclick="clickFunc(event)" style="text-align: center;margin:15px;
border:1px solid red;border-radius:3px;">
<div style="margin: 25px; border:1px solid royalblue;border-radius:3px;">
<div style="margin:25px;border:1px solid skyblue;border-radius:3px;">
<button style="margin:10px">
Button
</button>
</div>
</div>
</div>
示例 JavaScript:
function clickFunc(event) {
console.log(event.target);
}
如果你点击按钮,尽管我们在最外层的 div
上附加了事件处理器,但会记录按钮的标记。因此可以得出结论,event.target
是触发事件的元素。
十三、event.currentTarget
是什么?
event.currentTarget
是显式附加事件处理器的元素。
复制第十二题中的 HTML 标记:
<div onclick="clickFunc(event)" style="text-align: center;margin:15px;
border:1px solid red;border-radius:3px;">
<div style="margin: 25px; border:1px solid royalblue;border-radius:3px;">
<div style="margin:25px;border:1px solid skyblue;border-radius:3px;">
<button style="margin:10px">
Button
</button>
</div>
</div>
</div>
然后稍微改变一下 JavaScript:
function clickFunc(event) {
console.log(event.currentTarget);
}
如果你点击按钮,会记录最外层的 div
的标记,即使我们点击的是按钮。在这个例子中,我们可以得出结论,event.currentTarget
是显式附加事件处理器的元素。
十四、==
和 ===
的区别是什么?
==
(抽象相等性)和 ===
(严格相等性)的区别在于,==
在进行比较前会对值进行强制转换,而 ===
则直接比较值和类型而不进行强制转换。
让我们深入探讨 ==
。首先谈谈强制转换。强制转换是指将一个值转换为另一种类型。在这个情况下,==
进行隐式强制转换。==
在比较两个值之前有一些条件需要满足。
假设我们要比较两个值 x
和 y
:
x == y
如果 x
和 y
类型相同,则直接比较它们。如果 x
是 null
而 y
是 undefined
,则返回 true
。反之亦然。如果 x
是数字类型而 y
是字符串类型,则将 y
转换为数字再比较。如果 x
是字符串类型而 y
是数字类型,则将 x
转换为数字再比较。如果 x
是布尔类型,则将其转换为数字再比较。如果 y
是布尔类型,则将 y
转换为数字再比较。如果 x
是字符串、符号或数字类型,而 y
是对象类型,则将 y
转换为其原始值再比较。如果 x
是对象类型,而 y
是字符串、符号或数字类型,则将 x
转换为其原始值再比较。否则返回 false
。
// 示例
let x = 5;
let y = 5;
console.log(x == y); // 输出 true
x = 1;
y = '1';
console.log(x == y); // 输出 true
x = null;
y = undefined;
console.log(x == y); // 输出 true
x = 0;
y = false;
console.log(x == y); // 输出 true
x = '1,2';
y = [1,2];
console.log(x == y); // 输出 true
x = '[object Object]';
y = {};
console.log(x == y); // 输出 true
以上示例均返回 true
。
使用 ===
运算符时,除了第一个示例外,其余都将返回 false
,因为它们类型不同。
x = 5;
y = 5;
console.log(x === y); // 输出 true
x = 1;
y = '1';
console.log(x === y); // 输出 false
x = null;
y = undefined;
console.log(x === y); // 输出 false
x = 0;
y = false;
console.log(x === y); // 输出 false
x = '1,2';
y = [1,2];
console.log(x === y); // 输出 false
x = '[object Object]';
y = {};
console.log(x === y); // 输出 false
十五、为什么在 JavaScript 中比较两个相似的对象会返回 false
?
假设我们有下面的例子:
let a = { a: 1 };
let b = { a: 1 };
let c = a;
console.log(a === b); // 输出 false 尽管它们具有相同的属性
console.log(a === c); // 输出 true 嗯...
JavaScript 分别对待对象和原始类型的比较。对于原始类型,它比较值;而对于对象,它比较引用或存储变量的内存地址。这就是为什么第一句返回 false
而第二句返回 true
。a
和 c
具有相同的引用,而 a
和 b
不同。
十六、!!
操作符做什么?
双否定操作符 !!
会将右边的值强制转换为布尔值。基本上,这是一种将值转换为布尔值的花哨方法。
console.log(!!null); // 输出 false
console.log(!!undefined); // 输出 false
console.log(!!''); // 输出 false
console.log(!!0); // 输出 false
console.log(!!NaN); // 输出 false
console.log(!!' '); // 输出 true
console.log(!!{}); // 输出 true
console.log(!![]); // 输出 true
console.log(!!1); // 输出 true
console.log(!![].length); // 输出 false
十七、如何在一行中评估多个表达式?
我们可以使用逗号操作符 ,
来在一行中评估多个表达式。它从左向右评估,并返回最右边项的值。
let x = 5;
x = (x++ , x = addFive(x), x *= 2, x -= 5, x += 10);
function addFive(num) {
return num + 5;
}
如果你记录 x
的值,它将是 27。首先,我们将 x
的值自增,它变成 6,然后调用 addFive
函数并将 6 作为参数传入,将结果赋值给新的 x
,它变为 11。之后,我们将 x
的当前值乘以 2 并赋值给更新后的 x
,它变为 22。接着,从 x
的当前值减去 5 并赋值给更新后的 x
,它变为 17。最后,将 x
的值加上 10 并赋值给更新后的 x
,现在 x
的值为 27。
十八、什么是提升(Hoisting)?
提升是指在定义变量或函数时,将它们移动到其作用域(全局或函数)顶部的过程。
为了理解提升,我们需要解释执行上下文。执行上下文是当前正在执行的代码环境。执行上下文有两个阶段:编译和执行。
-
编译阶段:在这个阶段,它获取所有函数声明并将它们提升到作用域的顶部,以便稍后引用它们;获取所有变量声明(使用
var
关键字声明)并同样提升它们,给它们分配默认值undefined
。 -
执行阶段:在这个阶段,它为先前提升的变量赋值,并执行或调用函数(对象中的方法)。
注意:只有函数声明和使用 var
关键字声明的变量被提升,函数表达式或箭头函数以及 let
和 const
关键字声明的变量不被提升。
假设我们在全局作用域中有以下示例代码:
console.log(y); // 输出 undefined
y = 1;
console.log(y); // 输出 1
console.log(greet("Mark")); // 输出 Hello Mark!
function greet(name){
return 'Hello ' + name + '!';
}
var y;
编译阶段看起来像这样:
function greet(name) {
return 'Hello ' + name + '!';
}
var y; // 隐式赋值为 undefined
// 等待“编译”阶段完成
// 然后开始“执行”阶段
/*
console.log(y);
y = 1;
console.log(y);
console.log(greet("Mark"));
*/
为了演示的目的,我对变量赋值和函数调用进行了注释。
编译阶段完成后,开始执行阶段,调用方法并对变量赋值。
function greet(name) {
return 'Hello ' + name + '!';
}
var y;
// 开始“执行”阶段
console.log(y);
y = 1;
console.log(y);
console.log(greet("Mark"));
十九、作用域是什么?
作用域是在 JavaScript 中我们能够有效访问变量或函数的区域。JavaScript 有三种类型的作用域:全局作用域、函数作用域和块作用域(ES6 引入)。
全局作用域
在全局命名空间中声明的变量或函数处于全局作用域,因此在整个代码范围内都可以访问。
// 全局命名空间
var g = "global"; // 在全局作用域中声明变量 g
function globalFunc(){
function innerFunc(){
console.log(g); // 可以访问 g 因为 g 是全局变量
}
innerFunc();
}
函数作用域
在函数内部声明的变量、函数及参数只在该函数内部可访问,但在函数外部则不可访问。
function myFavoriteFunc(a) { // 函数作用域
if (true) {
var b = "Hello " + a; // a 和 b 在这个函数内部可访问
}
return b;
}
myFavoriteFunc("World");
console.log(a); // 抛出 ReferenceError "a" 未定义
console.log(b); // 不继续执行
块作用域
使用 let
或 const
在块 {}
内部声明的变量只能在其内部访问。
function testBlock(){
if(true){
let z = 5; // z 在这个 if 块内可访问
}
return z; // 抛出 ReferenceError "z" 未定义
}
testBlock();
作用域也是查找变量的一套规则。如果当前作用域中不存在某个变量,它就会向上查找该变量。如果仍然找不到,则继续向上查找,直到达到全局作用域。如果找到了变量,则可以使用它;否则抛出错误。它搜索最近的变量,并且一旦找到就停止查找。这称为作用域链。
/* 作用域链
从内部函数的视角来看
内部作用域 -> 外部作用域 -> 全局作用域
*/
// 全局作用域
var variable1 = "Comrades";
var variable2 = "Sayonara";
function outer(){
// 外部作用域
var variable1 = "World";
function inner(){
// 内部作用域
var variable2 = "Hello";
console.log(variable2 + " " + variable1);
}
inner();
}
outer(); // 输出 Hello World
// 因为 (variable2 = "Hello") 和 (variable1 = "World") 是在内部作用域中最接近的变量
二十、什么是闭包?
闭包可能是一个有争议的话题,但我将从我的理解来解释。
闭包简单来说就是函数在声明时能够记住其当前作用域、父级函数作用域、父的父级函数作用域直至到达全局作用域的变量和参数的引用的能力。基本上,它是函数声明时创建的作用域。
举例是解释闭包的一个好方式。
// 全局作用域
var globalVar = "abc";
function a(){
// a 的作用域
console.log(globalVar);
}
a(); // 输出 "abc"
/* 作用域链
从 a 函数的视角来看
a 的作用域 -> 全局作用域
*/
在这个例子中,当我们声明函数时,全局作用域成为了闭包的一部分。
a 的闭包
图中变量没有值的原因是因为该变量的值可能会根据我们何时何地调用函数而变化。但是,在上面的例子中,变量 globalVar
的值是 "abc"。
让我们看一个复杂一点的例子。
var globalVar = "global";
var outerVar = "outer";
function outerFunc(outerParam) {
function innerFunc(innerParam) {
console.log(globalVar, outerParam, innerParam);
}
return innerFunc;
}
const x = outerFunc(outerVar);
outerVar = "outer-2";
globalVar = "guess";
x("inner");
这将输出 "guess outer inner"。解释如下:当我们调用 outerFunc
并将返回的函数赋值给变量 x
时,outerParam
的值为 "outer",即使后来我们给 outerVar
赋了一个新值 "outer-2"。这是因为重新赋值发生在 outerFunc
调用之后,在调用 outerFunc
时,通过作用域链查找 outerParam
的值,outerParam
的值为 "outer"。现在,当我们调用变量 x
,它引用了 innerFunc
,innerParam
的值为 "inner",因为我们传递了这个值。而变量 globalVar
的值为 "guess",因为在调用 x
之前我们给 globalVar
赋了一个新值,并且在作用域链中 globalVar
的值为 "guess"。
下面是一个展示不正确理解闭包所导致的问题的例子。
const arrFuncs = [];
for(var i = 0; i < 5; i++){
arrFuncs.push(function (){
return i;
});
}
console.log(i); // i 的值为 5
for (let i = 0; i < arrFuncs.length; i++) {
console.log(arrFuncs[i]()); // 所有输出都是 "5"
}
这段代码并没有如预期那样工作,原因在于闭包。关键字 var
使得 i
成为一个全局变量,当我们推送函数时,返回的是全局变量 i
。因此当我们在循环结束后调用数组中的任何一个函数时,它输出 "5",因为此时 i
的当前值为 5,并且我们可以访问到它因为它是一个全局变量。由于闭包保留的是变量的引用而不是创建时的值,我们可以通过使用立即执行函数表达式(IIFE)或者将 var
改为 let
来解决这个问题。
二十一、JavaScript 中的假值有哪些?
假值指的是那些在转化为布尔值时会成为 false
的值。
const falsyValues = ['', 0, null, undefined, NaN, false];
二十二、如何检查一个值是否为假值?
↑ 使用 Boolean
函数或者双重否定运算符 !!
。
// 使用 Boolean 函数
Boolean(''); // 返回 false
Boolean(0); // 返回 false
Boolean(null); // 返回 false
Boolean(undefined); // 返回 false
Boolean(NaN); // 返回 false
Boolean(false); // 返回 false
// 使用 !! 运算符
!!''; // 返回 false
!!0; // 返回 false
!!null; // 返回 false
!!undefined; // 返回 false
!!NaN; // 返回 false
!!false; // 返回 false
二十三、“use strict” 是什么?
↑ “use strict” 是 JavaScript 中 ES5 的特性,它使我们的代码进入严格模式(Strict Mode),无论是在函数还是整个脚本中。严格模式帮助我们尽早避免代码中的 bug,并增加了对代码的限制。
严格模式带来的限制:
-
分配或访问未声明的变量。
function returnY() {
"use strict";
// y = 123; // 将抛出错误,因为 y 没有声明
return y;
}
-
给只读或不可写的全局变量分配值;
"use strict";
// var NaN = NaN; // 抛出错误,因为 NaN 是只读属性
// var undefined = undefined; // 抛出错误,因为 undefined 是只读属性
// var Infinity = "and beyond"; // 抛出错误,因为 Infinity 是只读属性
-
删除不可删除的属性。
"use strict";
const obj = {};
Object.defineProperty(obj, 'x', {
value: '1',
writable: false,
configurable: false
});
// delete obj.x; // 抛出错误,因为 x 是不可删除的
-
重复的参数名称。
"use strict";
function someFunc(a, b, b, c) {
// 抛出错误,因为 b 作为参数名出现了两次
}
-
使用 eval 函数创建变量。
"use strict";
eval("var x = 1;");
// console.log(x); // 抛出 ReferenceError,x 未定义
-
默认情况下 this
的值为undefined
。
"use strict";
function showMeThis() {
return this;
}
showMeThis(); // 返回 undefined
严格模式中有许多其他的限制。
二十四、在 JavaScript 中 this
的值是什么?
↑ this
基本上指的是当前执行或调用函数的对象的值。之所以说“当前”,是因为 this
的值取决于我们使用它的上下文以及使用的地点。
const carDetails = {
name: "Ford Mustang",
yearBought: 2005,
getName() {
return this.name;
},
isRegistered: true
};
console.log(carDetails.getName()); // 输出 Ford Mustang
这是我们可以预料的情况,因为在 getName
方法中返回 this.name
,这里的 this
指的是当前拥有该方法的对象 carDetails
。
让我们添加一些代码来使情况变得有些奇怪。在上述语句下方添加以下三行代码。
var name = "Ford Ranger";
var getCarName = carDetails.getName;
console.log(getCarName()); // 输出 Ford Ranger
第二次调用打印出“Ford Ranger”,这很奇怪,因为第一次调用打印出了“Ford Mustang”。原因是 getCarName
方法有一个不同的“所有者”对象,即 window
对象。在全局作用域中使用 var
关键字声明的变量会被附加到同名的 window
对象上。记住,在全局作用域中,当没有使用 use strict
时,this
指向 window
对象。
console.log(getCarName === window.getCarName); // 输出 true
console.log(getCarName === this.getCarName); // 输出 true
在这个例子中,this
和 window
指的是同一个对象。
解决这个问题的一种方法是使用函数的 .apply()
和 .call()
方法。
console.log(getCarName.apply(carDetails)); // 输出 Ford Mustang
console.log(getCarName.call(carDetails)); // 输出 Ford Mustang
.apply()
和 .call()
方法期望第一个参数是一个对象,该对象将成为 this
的值。
立即执行函数表达式(IIFE)、在全局作用域中声明的函数、匿名函数以及对象方法中的嵌套函数默认 this
指向 window
对象。
(function () {
console.log(this); // 输出 "window" 对象
})();
function iHateThis() {
console.log(this);
}
iHateThis(); // 输出 "window" 对象
const myFavoriteObj = {
guessThis() {
function getThis() {
console.log(this);
}
getThis();
},
name: 'Marko Polo',
thisIsAnnoying(callback) {
callback();
}
};
myFavoriteObj.guessThis(); // 输出 "window" 对象
myFavoriteObj.thisIsAnnoying(function () {
console.log(this); // 输出 "window" 对象
});
如果我们想要获取对象属性 name
的值 Marko Polo
,有两种方法可以解决。 首先,我们将 this
的值保存在一个变量中。
const myFavoriteObj = {
guessThis() {
const self = this; // 保存 `this` 的值到 `self` 变量
function getName() {
console.log(self.name);
}
getName();
},
name: 'Marko Polo',
thisIsAnnoying(callback) {
callback();
}
};
在这个例子中,我们将 this
的值保存下来,该值为 myFavoriteObj
。这样我们就可以在内部函数 getName
中访问它。
其次,我们可以使用 ES6 箭头函数。
const myFavoriteObj = {
guessThis() {
const getName = () => {
// 复制 `this` 在箭头函数外部的值
console.log(this.name);
}
getName();
},
name: 'Marko Polo',
thisIsAnnoying(callback) {
callback();
}
};
箭头函数没有自己的 this
。它复制了外围词法环境中的 this
值。在这个例子中,this
在外部函数中的值为 myFavoriteObj
。
二十五、对象的原型是什么?
↑ 原型(prototype
)简单来说就是一个对象的蓝图。它用于作为属性和方法的回退选项,如果这些属性或方法在当前对象中不存在的话。它是共享对象之间属性和功能的方式。它是 JavaScript 原型继承的核心概念。
const o = {};
console.log(o.toString()); // 输出 [object Object]
尽管 toString
方法并不存在于对象 o
中,但它不会抛出错误而是返回字符串 [object Object]
。当一个属性在对象中不存在时,它会在其原型中查找,如果仍然不存在,则继续在其原型的原型中查找,以此类推,直到在原型链中找到具有相同名称的属性为止。原型链的终点是 Object.prototype
。
console.log(o.toString === Object.prototype.toString); // 输出 true
// 这意味着我们在查找原型链时达到了 `Object.prototype` 并使用了 `toString` 方法。
结论
-
使用 Boolean
函数或双重否定运算符可以检测一个值是否为假值。 -
“use strict” 使代码进入严格模式,增加了对代码的限制。 -
this
的值取决于函数的调用上下文。 -
解决 this
的指向问题可以使用.apply()
和.call()
方法或保存this
的值。 -
原型用于实现对象间的属性共享,是 JavaScript 原型继承的基础。
二十六、什么是 IIFE?它有什么用途?
↑ 立即执行函数表达式(IIFE)是指在声明后立即被执行的函数。创建 IIFE 的语法是将函数包裹在圆括号(组操作符)中,使其被视为一个表达式,然后紧接着再用一对圆括号来调用它。所以 IIFE 看起来像这样 function (){}()
。
(function(){})(); // 简单的 IIFE 示例
(function () {
})(); // 另一个简单的 IIFE 示例
(function () {
})(); // 还有一个 IIFE 示例
(function named(params) {
})(); // 命名的 IIFE 示例
(() => {
})(); // 使用箭头函数的 IIFE
(function (global) {
})(window); // 向 IIFE 传参
const utility = (function () {
return {
// utilities
};
})();
以上示例均为有效的 IIFE。倒数第二个示例展示了我们可以向 IIFE 传递参数。最后一个示例展示了我们可以将 IIFE 的结果保存到一个变量中以便后续引用。
IIFE 最佳用途之一是初始化设置功能,并避免与全局作用域中的其他变量发生命名冲突或污染全局命名空间。我们来看一个例子。
<script src="https://cdnurl.com/somelibrary.js"></script>
假设我们链接了一个库文件 somelibrary.js
,该库暴露了一些全局函数供我们在代码中使用,但该库中有两个我们不使用的方法 createGraph
和 drawGraph
,并且这两个方法存在 bug。我们想实现自己的 createGraph
和 drawGraph
方法。
一种解决方案是改变我们的脚本结构。
<script src="https://cdnurl.com/somelibrary.js"></script>
<script>
function createGraph() {
// createGraph 逻辑在这里
}
function drawGraph() {
// drawGraph 逻辑在这里
}
</script>
这种方法会覆盖库提供的方法。
另一种方法是更改我们自己的辅助函数的名字。
<script src="https://cdnurl.com/somelibrary.js"></script>
<script>
function myCreateGraph() {
// createGraph 逻辑在这里
}
function myDrawGraph() {
// drawGraph 逻辑在这里
}
</script>
采用此方法需要更改对这些函数的调用,使用新的函数名。
还有一种方法是使用 IIFE。
<script src="https://cdnurl.com/somelibrary.js"></script>
<script>
const graphUtility = (function () {
function createGraph() {
// createGraph 逻辑在这里
}
function drawGraph() {
// drawGraph 逻辑在这里
}
return {
createGraph,
drawGraph
}
})();
</script>
这里,我们创建了一个工具变量 graphUtility
,它是 IIFE 的结果,返回一个包含 createGraph
和 drawGraph
方法的对象。
IIFE 解决的另一个问题是这样的例子。
var li = document.querySelectorAll('.list-group > li');
for (var i = 0, len = li.length; i < len; i++) {
li[i].addEventListener('click', function (e) {
console.log(i);
})
}
假设我们有一个带有 list-group
类的元素,它有五个子元素。我们希望点击每个子元素时能打印出当前索引值 i
。 但这个代码并不能如愿,而是每次点击都会打印 5
。这是因为闭包的工作原理,当我们在全局作用域中声明变量 i
时,它成为一个全局变量。所以当我们稍后在回调函数中引用 i
时,它打印的是 5
,因为那是最后的值。
一个解决办法是使用 IIFE。
var li = document.querySelectorAll('.list-group > li');
for (var i = 0, len = li.length; i < len; i++) {
(function (currentIndex) {
li[currentIndex].addEventListener('click', function (e) {
console.log(currentIndex);
})
})(i);
}
此方法有效的原因是每次迭代时 IIFE 创建一个新的作用域,并捕获 i
的值并将其作为参数传递给 currentIndex
,这样每次调用 IIFE 时 currentIndex
的值都不同。
二十七、Function.prototype.apply
方法的用途是什么?
↑ apply
方法指定调用函数时的 this
或者说是函数在那个时刻的“拥有者”对象。
const details = {
message: 'Hello World!'
};
function getMessage(){
return this.message;
}
getMessage.apply(details); // 返回 'Hello World!'
该方法类似于 Function.prototype.call
,不同之处在于我们如何传递参数。在 apply
中,我们以数组形式传递参数。
const person = {
name: "Marko Polo"
};
function greeting(greetingMessage) {
return `${greetingMessage} ${this.name}`;
}
greeting.apply(person, ['Hello']); // 返回 "Hello Marko Polo!"
二十八、Function.prototype.call
方法的用途是什么?
↑ call
方法指定调用函数时的 this
或者说是函数在那个时刻的“拥有者”对象。
const details = {
message: 'Hello World!'
};
function getMessage(){
return this.message;
}
getMessage.call(details); // 返回 'Hello World!'
该方法类似于 Function.prototype.apply
,不同之处在于我们如何传递参数。在 call
中,我们直接以逗号分隔的形式传递参数。
const person = {
name: "Marko Polo"
};
function greeting(greetingMessage) {
return `${greetingMessage} ${this.name}`;
}
greeting.call(person, 'Hello'); // 返回 "Hello Marko Polo!"
二十九、Function.prototype.apply
和 Function.prototype.call
之间的区别是什么?
↑ apply
和 call
之间的唯一区别在于我们如何传递调用函数的参数。在 apply
中,我们以数组形式传递参数,而在 call
中,我们直接在参数列表中传递参数。
const obj1 = {
result:0
};
const obj2 = {
result:0
};
function reduceAdd(){
let result = 0;
for(let i = 0, len = arguments.length; i < len; i++){
result += arguments[i];
}
this.result = result;
}
reduceAdd.apply(obj1, [1, 2, 3, 4, 5]); // 返回 15
reduceAdd.call(obj2, 1, 2, 3, 4, 5); // 返回 15
三十、Function.prototype.bind
的用途是什么?
↑ bind
方法返回一个绑定到特定 this
值或“拥有者”对象的新函数,以便我们可以在代码中稍后使用。call
和 apply
方法立即调用函数,而 bind
方法则返回一个新的函数。
import React from 'react';
class MyComponent extends React.Component {
constructor(props){
super(props);
this.state = {
value : ""
}
this.handleChange = this.handleChange.bind(this);
// 将 "handleChange" 方法绑定到 "MyComponent" 组件
}
handleChange(e){
// 在这里做些神奇的事情
}
render(){
return (
<>
<input type={this.props.type}
value={this.state.value}
onChange={this.handleChange}
/>
</>
)
}
}
结论
-
IIFE 是立即执行的函数表达式,常用于初始化设置并避免全局命名空间污染。 -
Function.prototype.apply
和call
都可以指定调用函数时的this
值,区别在于参数的传递方式。 -
Function.prototype.bind
用于返回一个绑定特定this
值的新函数。
三十一、什么是函数式编程?JavaScript 的哪些特性使得它可以作为函数式语言?
↑ 函数式编程是一种声明式的编程范式或模式,通过使用表达式来构建应用程序而不改变或突变传递给它的参数。
JavaScript 数组具有 map
, filter
, reduce
方法,这些是在函数式编程世界中最著名的函数,因为它们非常有用且不会改变原始数组,使得这些函数成为纯函数。此外,JavaScript 支持闭包和高阶函数,这些都是函数式编程语言的特征。
-
map
方法创建了一个新数组,其结果是通过调用提供的回调函数对原数组的每一个元素进行处理得到的。
const words = ["Functional", "Procedural", "Object-Oriented"];
const wordsLength = words.map(word => word.length);
-
filter
方法创建了一个新数组,其中包含了所有通过回调函数测试的元素。
const data = [
{ name: 'Mark', isRegistered: true },
{ name: 'Mary', isRegistered: false },
{ name: 'Mae', isRegistered: true }
];
const registeredUsers = data.filter(user => user.isRegistered);
-
reduce
方法通过从左到右对数组中的每一个元素应用一个函数与累加器相结合,从而减少为单一值。
const strs = ["I", " ", "am", " ", "Iron", " ", "Man"];
const result = strs.reduce((acc, currentStr) => acc + currentStr, "");
三十二、什么是高阶函数?
↑ 高阶函数是可以返回一个函数或接收一个或多个函数作为参数的函数。
function higherOrderFunction(param, callback) {
return callback(param);
}
三十三、为什么函数被称为一等公民?
↑ JavaScript 中的函数是一等公民,因为它们像语言中的任何其他值一样被对待。它们可以被赋值给变量,可以作为对象的属性(称为方法),可以作为数组的项,可以作为函数的参数传递,并且可以作为函数的返回值。函数与其他值唯一的不同在于函数可以被调用或执行。
三十四、手动实现 Array.prototype.map
方法
↑
function map(arr, mapCallback) {
if (!Array.isArray(arr) || !arr.length || typeof mapCallback !== 'function') {
return [];
} else {
let result = [];
for (let i = 0, len = arr.length; i < len; i++) {
result.push(mapCallback(arr[i], i, arr));
}
return result;
}
}
正如 MDN 对 Array.prototype.map
方法的描述: map()
方法创建了一个新数组,其结果是通过调用提供的函数对原数组的每一个元素进行处理得到的。
三十五、手动实现 Array.prototype.filter
方法
↑
function filter(arr, filterCallback) {
if (!Array.isArray(arr) || !arr.length || typeof filterCallback !== 'function') {
return [];
} else {
let result = [];
for (let i = 0, len = arr.length; i < len; i++) {
if (filterCallback(arr[i], i, arr)) {
result.push(arr[i]);
}
}
return result;
}
}
正如 MDN 对 Array.prototype.filter
方法的描述: filter()
方法创建了一个新数组,其中包含了所有通过提供的函数测试的元素。
三十六、手动实现 Array.prototype.reduce
方法
↑
function reduce(arr, reduceCallback, initialValue) {
if (!Array.isArray(arr) || !arr.length || typeof reduceCallback !== 'function') {
return [];
} else {
let hasInitialValue = initialValue !== undefined;
let value = hasInitialValue ? initialValue : arr[0];
for (let i = hasInitialValue ? 0 : 1, len = arr.length; i < len; i++) {
value = reduceCallback(value, arr[i], i, arr);
}
return value;
}
}
正如 MDN 对 Array.prototype.reduce
方法的描述: reduce()
方法对数组中的每一个元素执行一个由您提供的缩减函数(即回调函数),最终计算为一个单一输出值。
三十七、arguments
对象是什么?
↑ arguments
对象是一个包含传递给函数的参数值的集合。它是一个类数组对象,因为它具有长度属性,并且我们可以使用数组索引符号访问个别值,但它没有数组内置的方法如 forEach
, reduce
, filter
, map
。它帮助我们了解传递给函数的参数数量。
我们可以使用 .slice
方法将 arguments
对象转换为数组。
function one() {
return Array.prototype.slice.call(arguments);
}
注意:arguments
对象在 ES6 箭头函数中不起作用。
const four = () => arguments;
当我们调用该函数时,它会抛出一个错误,因为我们可以在支持剩余参数语法的环境中解决这个问题。
const four = (...args) => args;
这会自动将所有参数值放入一个数组中。
三十八、如何创建一个没有原型的对象?
↑ 我们可以使用 Object.create
方法来创建一个没有原型的对象。
const o1 = {};
console.log(o1.toString()); // 输出 [object Object]
const o2 = Object.create(null);
console.log(o2.toString()); // 抛出错误 o2.toString is not a function
这里的第一个参数是对象 o2
的原型,在这种情况下将是 null
,表明我们不想要任何原型。
三十九、在这段代码中,为什么当调用此函数时 b
成为了全局变量?
↑
function myFunc() {
let a = b = 0;
}
myFunc();
原因是赋值运算符 =
具有从右到左的结合性或求值性。这意味着当在一个表达式中出现多个赋值运算符时,它们是从右到左求值的。因此,我们的代码实际上变成了下面的样子。
function myFunc() {
let a = (b = 0);
}
myFunc();
首先,表达式 b = 0
被求值,在这个例子中 b
没有被声明。因此,JS 引擎会在函数外部创建一个全局变量 b
,之后表达式的返回值为 0
,并将它赋值给用 let
关键字声明的新局部变量 a
。
我们可以通过首先声明变量然后再给它们赋值来解决这个问题。
function myFunc() {
let a, b;
a = b = 0;
}
myFunc();
四十、ECMAScript 是什么?
↑ ECMAScript 是一个为脚本语言制定的标准,这意味着 JavaScript 遵循 ECMAScript 标准中的规范变化,因为它是 JavaScript 的蓝图。
四十一、ES6 或 ECMAScript 2015 中的新特性
ES6,也称作 ECMAScript 2015,是 JavaScript 的一个重要版本更新,引入了许多新的特性和语法糖,包括但不限于以下特性:
-
箭头函数 ( =>
) 提供了一种更为简洁的方式来定义函数。 -
类 ( class
) 使得面向对象编程更加直观。 -
模板字符串 ( -
增强的对象字面量 ( {}
) 使得创建对象更加便捷。 -
对象解构赋值 ( {}
) 可以从复杂对象中提取值。 -
Promise 一种用于处理异步操作的新模式。 -
生成器函数 ( function*
) 用于创建迭代器。 -
模块 ( import/export
) 支持模块化开发。 -
Symbol 类型提供唯一的标识符。 -
代理 ( Proxy
) 用于拦截并定义自定义行为。 -
集合 ( Set
) 存储唯一值的数据结构。 -
默认函数参数 ( function(a=1)
) 定义函数参数的默认值。 -
剩余参数与展开操作符 ( ...
) 用于收集剩余参数或展开数组。 -
块作用域变量 ( let
和const
) 提供了更严格的变量作用域。
四十二、 var
, let
, const
关键字之间的区别
-
变量声明与作用域
-
使用 var
关键字声明的变量具有函数作用域,这意味着即使在某个代码块中声明,也可以在整个函数内部访问到。
function giveMeX(showX) { // 函数作用域
if (showX) { // 块作用域
var x = 5; // 在整个函数中都可以访问
}
return x;
}-
使用 let
和const
关键字声明的变量具有块作用域,只能在声明它们的代码块内访问。
function giveMeX(showX) {
if (showX) {
let x = 5; // 仅在 if 块内可访问
}
return x; // 抛出引用错误
} -
-
重新赋值
-
var
和let
声明的变量可以重新赋值,而const
声明的常量则不可以。
let y = 5; // 可以重新赋值
y = 10;
const z = 5; // 不可以重新赋值
z = 10; // 抛出 TypeError -
四十三、什么是箭头函数
箭头函数是 JavaScript 中定义函数的一种新方式。它们简化了函数定义的语法,并且不绑定自己的 this
值。
// ES5 版本
var getCurrentDate = function (){ // 声明函数
return new Date(); // 返回当前日期
};
// ES6 版本
const getCurrentDate = () => new Date(); // 箭头函数,隐式返回
如果箭头函数只有一条表达式,则不需要显式写 return
,直接返回即可。
// ES5 版本
function greet(name) {
return 'Hello ' + name + '!'; // 字符串拼接
}
// ES6 版本
const greet = (name) => `Hello ${name}!`; // 模板字符串
箭头函数还可以接收参数,并且如果只有一个参数,则可以省略括号。
const getArgs = () => arguments; // 获取参数对象
const getArgs2 = (...rest) => rest; // 使用剩余参数
箭头函数不拥有自己的 arguments
对象,所以如果尝试通过箭头函数访问 arguments
,将会抛出错误。但是,可以使用剩余参数来代替。
const getArgs = () => arguments; // 抛出错误
const getArgs2 = (...rest) => rest; // 正确获取参数
箭头函数不创建自己的 this
上下文;它们继承自定义义它们的作用域的 this
值。
const data = {
result: 0,
nums: [1, 2, 3, 4, 5],
computeResult() { // 此处的 this 指向 data 对象
const addAll = () => { // 箭头函数继承外层函数的 this
return this.nums.reduce((total, cur) => total + cur, 0);
};
this.result = addAll();
}
};
四十四、什么是类
类是 JavaScript 中定义构造函数的一种新方式。它们提供了更清晰的语法糖,但仍基于原型继承。
// ES5 版本
function Person(firstName, lastName, age, address) { // 构造函数
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.address = address;
}
Person.prototype.toString = function() { // 原型方法
return "[object Person]";
};
// ES6 版本
class Person { // 类定义
constructor(firstName, lastName, age, address) { // 构造器
this.lastName = lastName;
this.firstName = firstName;
this.age = age;
this.address = address;
}
toString() { // 类方法
return "[object Person]";
}
}
类支持继承和方法覆盖。
// ES5 版本
function Employee(firstName, lastName, age, address, jobTitle, yearStarted) {
Person.call(this, firstName, lastName, age, address); // 继承父类属性
this.jobTitle = jobTitle;
this.yearStarted = yearStarted;
}
Employee.prototype = Object.create(Person.prototype); // 继承父类原型
Employee.prototype.constructor = Employee;
// ES6 版本
class Employee extends Person { // 继承类
constructor(firstName, lastName, age, address, jobTitle, yearStarted) {
super(firstName, lastName, age, address); // 调用父类构造器
this.jobTitle = jobTitle;
this.yearStarted = yearStarted;
}
toString() { // 覆盖父类方法
return "[object Employee]";
}
}
尽管 ES6 类看起来像是面向对象的语言中的类,但实际上它们依然是基于原型的继承。
class Something { // 类定义
}
function AnotherSomething() {} // 函数定义
const as = new AnotherSomething(); // 创建实例
const s = new Something();
console.log(typeof Something); // logs "function"
console.log(typeof AnotherSomething); // logs "function"
console.log(as.toString()); // logs "[object Object]"
console.log(s.toString()); // logs "[object Object]"
console.log(as.toString === Object.prototype.toString); // true
console.log(s.toString === Object.prototype.toString); // true
四十五、模板字符串是什么
模板字符串是 JavaScript 中定义字符串的一种新方式。使用反引号 (
) 来创建,并允许嵌入变量和表达式。
// ES5 版本
var greet = 'Hi I\'m Mark'; // 使用转义字符
// ES6 版本
let greet = `Hi I'm Mark`; // 直接书写
模板字符串允许更自然地处理多行文本。
// ES5 版本
var lastWords = '\n' +
' I \n' +
' Am \n' +
'Iron Man \n'; // 使用换行符
// ES6 版本
let lastWords = `
I
Am
Iron Man
`; // 自动换行
模板字符串还可以嵌入表达式。
// ES5 版本
function greet(name) {
return 'Hello ' + name + '!'; // 字符串拼接
}
// ES6 版本
const greet = name => {
return `Hello ${name} !`; // 嵌入表达式
}
使用模板字符串可以避免繁琐的字符串拼接,并且使代码更易读。
结论
-
ES6 引入了许多新特性,如箭头函数、类、模板字符串等,使得 JavaScript 更加强大和易于使用。 -
var
,let
,const
三个关键字分别表示不同的变量作用域,选择合适的关键字可以帮助开发者更好地管理代码。 -
箭头函数简化了函数定义的方式,并且改变了 this
的绑定规则。 -
类提供了面向对象编程的一个更简洁的语法,但底层仍然是基于原型的继承机制。 -
模板字符串提供了一种更方便的方式来创建和格式化字符串,提高了代码的可读性。
四十六、什么是对象解构?
对象解构是一种从对象或数组中提取值的新方法,它使得代码更加简洁和易读。
假设我们有一个如下所示的对象:
const employee = {
firstName: "Marko", // 员工的名字
lastName: "Polo", // 员工的姓氏
position: "Software Developer", // 员工的职位
yearHired: 2017 // 员工入职年份
};
以前获取对象属性的方式是为每个属性创建一个同名的变量,这样做很麻烦,尤其是当对象很大并且包含许多属性和方法时。
var firstName = employee.firstName; // 获取名字
var lastName = employee.lastName; // 获取姓氏
var position = employee.position; // 获取职位
var yearHired = employee.yearHired; // 获取入职年份
使用对象解构可以使代码更加清晰,并且比传统方式更节省时间。对象解构的语法是在大括号 {}
内指定想要提取的属性。
let { firstName, lastName, position, yearHired } = employee; // 解构赋值
如果我们想改变提取的变量名称,可以使用以下语法:
let { firstName: fName, lastName: lName, position, yearHired } = employee; // 重命名属性
// 这里 fName 将持有 firstName 属性的值,lName 将持有 lastName 属性的值
在解构赋值时,我们还可以设置默认值。如果对象中不存在该属性,那么将使用提供的默认值。
let { firstName = "Mark", lastName: lName, position, yearHired } = employee; // 设置默认值
四十七、ES6 模块是什么?
模块让我们能够将代码分割成多个文件以提高可维护性,并避免将所有代码放在一个大文件中。在 ES6 支持模块之前,有两个流行的模块系统被用来提高 JavaScript 代码的可维护性:
-
CommonJS - 主要用于 Node.js -
AMD (Asynchronous Module Definition) - 主要用于浏览器环境
基本上,使用模块的语法非常直接,import
用来从另一个文件中导入功能或多个功能或值,而 export
则用来暴露文件中的功能或多个功能或值。
// 导出功能(Named Exports)
// 使用 ES5 CommonJS - helpers.js
exports.isNull = function (val) {
return val === null;
}
exports.isUndefined = function (val) {
return val === undefined;
}
exports.isNullOrUndefined = function (val) {
return exports.isNull(val) || exports.isUndefined(val);
}
// 使用 ES6 Modules - helpers.js
export function isNull(val) {
return val === null;
}
export function isUndefined(val) {
return val === undefined;
}
export function isNullOrUndefined(val) {
return isNull(val) || isUndefined(val);
}
// 导入功能(Importing functionalites in another File)
// 使用 ES5 CommonJS - index.js
const helpers = require('./helpers.js'); // helpers 是一个对象
const isNull = helpers.isNull;
const isUndefined = helpers.isUndefined;
const isNullOrUndefined = helpers.isNullOrUndefined;
// 如果你的环境支持解构赋值
const { isNull, isUndefined, isNullOrUndefined } = require('./helpers.js');
// 使用 ES6 Modules - index.js
import * as helpers from './helpers.js'; // helpers 是一个对象
// 或者
import { isNull, isUndefined, isNullOrUndefined as isValid } from './helpers.js';
// 使用 "as" 为命名导出重命名
// 导出单个功能(Default Exports)
// 使用 ES5 CommonJS - index.js
class Helpers {
static isNull(val) {
return val === null;
}
static isUndefined(val) {
return val === undefined;
}
static isNullOrUndefined(val) {
return this.isNull(val) || this.isUndefined(val);
}
}
module.exports = Helpers;
// 使用 ES6 Modules - helpers.js
class Helpers {
static isNull(val) {
return val === null;
}
static isUndefined(val) {
return val === undefined;
}
static isNullOrUndefined(val) {
return this.isNull(val) || this.isUndefined(val);
}
}
export default Helpers;
// 导入单个功能
// 使用 ES5 CommonJS - index.js
const Helpers = require('./helpers.js');
console.log(Helpers.isNull(null));
// 使用 ES6 Modules
import Helpers from './helpers.js';
console.log(Helpers.isNull(null));
这是使用 ES6 模块的基础知识。由于模块是一个广泛的话题,这里只是简单介绍了基本概念。
四十八、Set
对象是什么以及它是如何工作的?
Set
对象是 ES6 的一个特性,它可以存储唯一的值,无论是原始类型还是对象引用。在一个 Set
中,一个值只能出现一次。它使用 SameValueZero
算法检查值是否已经存在于 Set
对象中。
我们可以使用构造函数创建 Set
实例,并且可以选择性地传递一个可迭代对象作为初始值。
const set1 = new Set(); // 创建空的 Set 实例
const set2 = new Set(["a", "b", "c", "d", "d", "e"]); // 创建带有初始值的 Set 实例
我们可以使用 add
方法向 Set
实例中添加新的值,并且由于 add
方法返回 Set
对象本身,因此可以链式调用。
set2.add("f"); // 添加一个新值
set2.add("g").add("h").add("i").add("j").add("k").add("k"); // 链式调用
// 最后一个 "k" 不会被添加到 Set 对象中,因为它已经存在
我们可以使用 delete
方法从 Set
实例中移除一个值,此方法返回一个布尔值,指示值是否存在。
set2.delete("k"); // 返回 true,因为 "k" 存在于 Set 对象中
set2.delete("z"); // 返回 false,因为 "z" 不存在于 Set 对象中
我们可以使用 has
方法检查特定值是否存在于 Set
实例中。
set2.has("a"); // 返回 true,因为 "a" 存在于 Set 对象中
set2.has("z"); // 返回 false,因为 "z" 不存在于 Set 对象中
我们可以使用 size
属性获取 Set
实例的长度。
set2.size; // 返回 10
我们可以使用 clear
方法删除 Set
实例中的所有元素。
set2.clear(); // 清空 Set 数据
我们可以利用 Set
对象来去除数组中的重复元素。
const numbers = [1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 5];
const uniqueNums = [...new Set(numbers)]; // 去除重复后的数组 [1, 2, 3, 4, 5, 6, 7, 8]
四十九、回调函数是什么?
回调函数是一种稍后执行的函数。
const btnAdd = document.getElementById('btnAdd'); // 获取按钮
btnAdd.addEventListener('click', function clickCallback(e) {
// 在按钮点击时执行一些无用的操作
});
在这个例子中,我们在 id 为 btnAdd
的元素上等待点击事件,如果点击发生,clickCallback
函数就会被执行。回调函数为某些数据或事件添加了一些额外的功能。例如,Array
的 reduce
、filter
和 map
方法都期望一个回调函数作为参数。一个很好的类比回调函数的例子是你打电话给别人,如果他们没有接听,你可以留言,并期待他们回拨。打电话或留言的行为就是事件或数据,而回拨则是预期发生的动作。
五十、Promises 是什么?
Promises 是处理 JavaScript 中异步操作的一种方式。它代表了一个异步操作的结果。Promises 设计出来是为了解决在使用回调处理异步代码时的问题。
在没有 Promises 之前,我们通常使用回调函数来处理异步操作:
fs.readFile('somefile.txt', function (e, data) { // 读取文件
if (e) {
console.log(e); // 处理错误
}
console.log(data); // 输出数据
});
这种方法的问题在于,如果我们的回调函数中还有另一个异步操作,再接着还有一个,代码会变得难以阅读和维护,这种代码被称为回调地狱(Callback Hell):
// 回调地狱
fs.readFile('somefile.txt', function (e, data) { // 读取文件
// 你的代码
fs.readdir('directory', function (e, files) { // 读取目录
// 你的代码
fs.mkdir('directory', function (e) { // 创建目录
// 你的代码
})
})
});
如果使用 Promises,上述代码会变得更加清晰易懂,也更容易维护:
promReadFile('file/path') // 读取文件
.then(data => { // 文件读取成功
return promReaddir('directory'); // 读取目录
})
.then(data => { // 目录读取成功
return promMkdir('directory'); // 创建目录
})
.catch(e => { // 捕获错误
console.log(e); // 输出错误
})
Promises 有三种不同的状态:
-
Pending - Promises 的初始状态。由于操作尚未完成,结果未知。 -
Fulfilled - 异步操作完成并成功,具有结果值。 -
Rejected - 异步操作失败,具有失败原因。
Promises 的构造函数有两个参数,分别是 resolve
和 reject
函数:
-
如果异步操作无错误地完成,则调用 resolve
函数来解析 Promise; -
如果发生错误,则调用 reject
函数,并传入错误或原因。
我们可以使用 .then
方法来访问已解决的 Promise 的结果,并在 .catch
方法中捕获错误。.then
方法返回一个 Promise,因此我们可以链式调用多个异步操作:
const myPromiseAsync = (...args) => { // 自定义异步函数
return new Promise((resolve, reject) => { // 创建一个新的 Promise
doSomeAsync(...args, (error, data) => { // 执行异步操作
if (error) { // 如果有错误
reject(error); // 拒绝 Promise
} else { // 如果没有错误
resolve(data); // 解析 Promise
}
})
})
}
myPromiseAsync() // 调用异步函数
.then(result => { // 成功后
console.log(result); // 输出结果
})
.catch(reason => { // 失败后
console.log(reason); // 输出失败原因
})
我们可以创建一个辅助函数,将带有回调的异步操作转换为 Promise。这类似于 Node.js 核心模块 util
提供的 promisify
功能:
const toPromise = (asyncFuncWithCallback) => { // 将回调函数转换为 Promise
return (...args) => { // 接收任意参数
return new Promise((res, rej) => { // 创建新的 Promise
asyncFuncWithCallback(...args, (e, result) => { // 执行异步函数
return e ? rej(e) : res(result); // 有错误则拒绝,否则解析
});
});
}
}
const promReadFile = toPromise(fs.readFile); // 转换 fs.readFile 为 Promise
promReadFile('file/path') // 读取文件路径
.then((data) => { // 成功后
console.log(data); // 输出数据
})
.catch(e => console.log(e)); // 失败后输出错误
五十一、async/await 是什么?它是如何工作的?
async/await 是一种新的编写异步或非阻塞代码的方法。它是基于 Promises 构建的,使得编写异步代码比使用 Promises 或回调更易读和更干净。但在使用此特性之前,你需要了解 Promises 的基础知识,因为 async/await 实际上仍然在底层使用 Promises。
使用 Promises:
function callApi() { // 定义一个调用 API 的函数
return fetch("url/to/api/endpoint") // 发起请求
.then(resp => resp.json()) // 解析响应为 JSON
.then(data => { // 处理数据
//do something with "data" // 使用数据做某事
})
.catch(err => { // 捕获错误
//do something with "err" // 处理错误
});
}
使用 Async/Await:
async function callApi() { // 使用 async 关键字定义异步函数
try { // 尝试
const resp = await fetch("url/to/api/endpoint"); // 等待请求完成
const data = await resp.json(); // 等待 JSON 解析完成
//do something with "data" // 使用数据做某事
} catch (e) { // 捕获异常
//do something with "err" // 处理错误
}
}
注意:async
关键字使函数隐式返回一个 Promise。
const giveMeOne = async () => 1; // 定义一个返回 1 的异步函数
giveMeOne() // 调用异步函数
.then((num) => { // 成功后
console.log(num); // 输出 1
});
注意:await
关键字只能在异步函数内部使用。在任何其他不是异步函数的地方使用 await
会抛出编译时错误。
const giveMeOne = async () => 1; // 定义一个返回 1 的异步函数
function getOne() { // 定义一个普通函数
try {
const num = await giveMeOne(); // 编译时错误
console.log(num);
} catch (e) {
console.log(e);
}
}
async function getTwo() { // 定义一个异步函数
try {
const num1 = await giveMeOne(); // 等待一个异步操作完成
const num2 = await giveMeOne(); // 等待另一个异步操作完成
return num1 + num2; // 返回两者的和
} catch (e) {
console.log(e);
}
}
await getTwo(); // 返回 2
五十二、扩展运算符(Spread Operator)与剩余参数(Rest Parameters)有何区别?
扩展运算符(Spread Operator)和剩余参数(Rest Parameters)使用相同的符号 ...
,但它们的作用不同。扩展运算符用于将数组或对象中的个别数据展开到另一个数据结构中,而剩余参数则用于在函数或数组中收集所有参数或值,并将它们放入数组中或从中提取部分值。
function add(a, b) { // 定义一个加法函数
return a + b; // 返回两数之和
};
const nums = [5, 6]; // 定义一个数组
const sum = add(...nums); // 使用扩展运算符展开数组
console.log(sum); // 输出 11
在这个例子中,当我们调用 add(...nums)
函数时,我们实际上是将数组展开。所以 a
的值为 5
,b
的值为 6
,因此 sum
为 11
。
function add(...rest) { // 定义一个接收任意数量参数的加法函数
return rest.reduce((total, current) => total + current); // 使用 reduce 方法计算总和
};
console.log(add(1, 2)); // 输出 3
console.log(add(1, 2, 3, 4, 5)); // 输出 15
在这个例子中,我们定义了一个函数 add
,它可以接受任意数量的参数并将它们相加。
const [first, ...others] = [1, 2, 3, 4, 5]; // 使用剩余参数解构数组
console.log(first); // 输出 1
console.log(others); // 输出 [2, 3, 4, 5]
在这个例子中,我们使用了剩余参数来提取数组中除了第一个元素之外的所有值,并将它们放入 others
数组中。
五十三、默认参数是什么?
默认参数是 JavaScript 中定义默认变量的一种新方式,它在 ES6 或 ECMAScript 2015 版本中可用。
// ES5 版本
function add(a, b) { // 定义一个加法函数
a = a || 0; // 如果 a 未定义,则赋值为 0
b = b || 0; // 如果 b 未定义,则赋值为 0
return a + b; // 返回两数之和
}
// ES6 版本
function add(a = 0, b = 0) { // 定义一个带有默认值的加法函数
return a + b; // 返回两数之和
}
// 如果不传递参数给 'a' 或 'b',则使用默认参数值 0
add(1); // 返回 1
我们也可以在默认参数中使用解构赋值:
function getFirst([first, ...rest] = [0, 1]) { // 定义一个获取第一个元素的函数
return first; // 返回第一个元素
}
getFirst(); // 返回 0
getFirst([10, 20, 30]); // 返回 10
function getArr({ nums } = { nums: [1, 2, 3, 4] }) { // 定义一个获取数组的函数
return nums; // 返回数组
}
getArr(); // 返回 [1, 2, 3, 4]
getArr({ nums: [5, 4, 3, 2, 1] }); // 返回 [5, 4, 3, 2, 1]
我们也可以使用前面定义的参数来影响后面定义的参数:
function doSomethingWithValue(value = "Hello World", callback = () => { console.log(value) }) { // 定义一个执行某个操作的函数
callback(); // 执行回调
}
doSomethingWithValue(); // 输出 "Hello World"
五十四、包装对象是什么?
像字符串、数字和布尔值这样的原始值即使它们不是对象,也有属性和方法。例外的是 null
和 undefined
。
let name = "marko"; // 定义一个字符串变量
console.log(typeof name); // 输出 "string"
console.log(name.toUpperCase()); // 输出 "MARKO"
尽管 name
是一个原始值,没有属性和方法,但我们在这里调用了一个 toUpperCase()
方法,这并没有抛出错误而是返回了 MARKO
。
原因是这个原始值暂时被转换成了一个对象,这样变量就表现得像一个对象一样。每一个原始值除了 null
和 undefined
都有包装对象。这些包装对象是 String
、Number
、Boolean
、Symbol
和 BigInt
。
在后台,调用看起来像是这样:
console.log(new String(name).toUpperCase()); // 输出 "MARKO"
新创建的对象在访问完属性或调用完方法后立即被丢弃。
五十五、隐式强制转换与显式强制转换有什么区别?
隐式强制转换是在不需程序员直接干预的情况下,将值转换为另一种类型的机制。
假设我们有下面的例子:
console.log(1 + '6'); // 输出 "16"
console.log(false + true); // 输出 "1"
console.log(6 * '2'); // 输出 12
第一句输出 16
。在其他语言中这可能会抛出编译错误,但在 JavaScript 中,1
被转换成了字符串 '1'
,然后与 '6'
拼接。我们没有做任何事情,但 JavaScript 自动为我们进行了转换。
第二句输出 1
,它将 false
转换成 0
,true
转换成 1
,结果为 1
。
第三句输出 12
,它在乘法前将字符串 '2'
转换成了数字 2
。
JavaScript 强制转换规则
显式强制转换是我们(程序员)明确进行的值到另一种类型的转换:
console.log(1 + parseInt('6')); // 输出 7
在这个例子中,我们使用 parseInt
函数将字符串 '6'
转换成数字 6
,然后将其与 1
相加。
五十六、NaN 是什么?如何检测一个值是否为 NaN?
NaN
表示“非数字”,在 JavaScript 中是一个特殊值,表示将一个非数字值转换为数字或对数字执行操作时的结果:
let a;
console.log(parseInt('abc')); // NaN, 字符串无法转换为整数
console.log(parseInt(null)); // 0, null 转换为整数
console.log(parseInt(undefined)); // NaN, undefined 无法转换为整数
console.log(parseInt(++a)); // NaN, a 未定义,++a 无效
console.log(parseInt({} * 10)); // NaN, 对象与数字相乘
console.log(parseInt('abc' - 2)); // NaN, 字符串与数字相减
console.log(parseInt(0 / 0)); // NaN, 0 除以 0
console.log(parseInt('10a' * 10)); // NaN, 含有非数字字符的字符串
JavaScript 提供了一个内置方法 isNaN
来测试一个值是否为 NaN
,但是这个函数的行为有些奇怪:
console.log(isNaN()); // logs true, 参数缺失时返回 true
console.log(isNaN(undefined)); // logs true, undefined 转换为数字时为 NaN
console.log(isNaN({})); // logs true, 对象转换为数字时为 NaN
console.log(isNaN(String('a'))); // logs true, 字符串 'a' 转换为数字时为 NaN
console.log(isNaN(() => { })); // logs true, 函数转换为数字时为 NaN
所有这些语句都返回 true
,即使我们传递的值并不是 NaN
。
在 ES6 或 ECMAScript 2015 中,推荐使用 Number.isNaN()
方法,因为它真正检测值是否为 NaN
,或者我们可以自己写一个帮助函数来检测这个问题,因为在 JavaScript 中 NaN
是唯一不等于自身的值:
function checkIfNaN(value) {
return value !== value; // NaN 是唯一不等于自身的值
}
五十七、如何检测一个值是否为数组?
我们可以通过使用 Array.isArray()
方法来检测一个值是否为数组。这个方法在 Array
全局对象上可用,当传入的参数是一个数组时返回 true
,否则返回 false
:
console.log(Array.isArray(5)); // logs false
console.log(Array.isArray("")); // logs false
console.log(Array.isArray()); // logs false
console.log(Array.isArray(null)); // logs false
console.log(Array.isArray({ length: 5 })); // logs false
console.log(Array.isArray([])); // logs true
如果你的环境不支持 Array.isArray()
方法,你可以使用下面的 polyfill 实现:
function isArray(value) {
return Object.prototype.toString.call(value) === "[object Array]";
}
五十八、如何在不使用 if
或模运算符 %
的情况下判断一个数字是否为偶数?
我们可以使用位与运算符 (&
) 来解决这个问题。位与运算符对操作数进行位级运算,并且对于偶数来说,最低位始终为 0
,而对于奇数来说,最低位始终为 1
:
function isEven(num) {
if (num & 1) { // 奇数
return false;
} else { // 偶数
return true;
}
}
以下是几个数字的二进制表示:
0 在二进制中是 000
1 在二进制中是 001
2 在二进制中是 010
3 在二进制中是 011
4 在二进制中是 100
5 在二进制中是 101
6 在二进制中是 110
7 在二进制中是 111
以此类推...
位与运算符的真值表:
a b a & b
0 0 0
0 1 0
1 0 0
1 1 1
当我们执行 console.log(5 & 1)
时,结果是 1
。首先,位与运算符将两个数字转换为二进制,5
变为 101
,1
变为 001
。 然后,它使用位与运算符比较每一位(0 和 1)。101 & 001
结果是 001
。 最后,二进制结果 001
被转换为十进制数 1
。 如果执行 console.log(4 & 1)
,结果是 0
。因为 4
的二进制形式是 100
,与 001
进行位与运算后得到 000
。 对于负数和 1
,我们可以使用递归函数来解决:
function isEven(num) {
if (num < 0 || num === 1) return false; // 负数或 1 都不是偶数
if (num == 0) return true; // 0 是偶数
return isEven(num - 2); // 递归减去 2 直到为 0 或 1
}
五十九、如何检测一个对象中是否存在某个属性?
有三种可能的方式来检测一个对象中是否存在某个属性:
-
使用 in
操作符。in
操作符的语法如下:propertyname in object
。如果属性存在则返回true
,否则返回false
:
const o = {
"prop" : "bwahahah",
"prop2" : "hweasa"
};
console.log("prop" in o); // logs true, 表明 prop 属性在对象 o 中存在
console.log("prop1" in o); // logs false, 表明 prop 属性不在对象 o 中
-
使用对象上的 hasOwnProperty
方法。此方法在 JavaScript 中所有对象上可用。如果属性存在则返回true
,否则返回false
:
console.log(o.hasOwnProperty("prop2")); // logs true
console.log(o.hasOwnProperty("prop1")); // logs false
-
使用方括号表示法 obj["prop"]
。如果属性存在则返回该属性的值,否则返回undefined
:
console.log(o["prop"]); // logs "bwahahah"
console.log(o["prop1"]); // logs undefined
六十、AJAX 是什么?
AJAX 是异步 JavaScript 和 XML 的缩写。它是一组相关技术,用于异步显示数据。这意味着可以在不重新加载网页的情况下发送数据到服务器并从服务器获取数据。
AJAX 使用的技术包括:
-
HTML - 网页结构 -
CSS - 网页样式 -
JavaScript - 网页行为及 DOM 更新 -
XMLHttpRequest API - 用于从服务器发送和检索数据 -
服务器端语言 - 如 PHP、Python、Node.js 等。
六十一、创建对象的方法有哪些?
-
使用对象字面量 const o = {
name: "Mark", // 设置 name 属性为 Mark
greeting() { // 定义一个方法 greeting
return `Hi, I'm ${this.name}`; // 返回带有 this.name 的问候语
}
};
o.greeting(); // 调用 greeting 方法返回 "Hi, I'm Mark" -
使用构造函数 function Person(name) { // 定义一个构造函数 Person
this.name = name; // 设置 this.name 属性为传入的名字
}
Person.prototype.greeting = function () { // 在原型上定义 greeting 方法
return `Hi, I'm ${this.name}`; // 返回带有 this.name 的问候语
}
const mark = new Person("Mark"); // 使用 new 关键字创建一个新的 Person 实例
mark.greeting(); // 调用 greeting 方法返回 "Hi, I'm Mark" -
使用 Object.create 方法 const n = {
greeting() { // 定义一个 greeting 方法
return `Hi, I'm ${this.name}`; // 返回带有 this.name 的问候语
}
};
const o = Object.create(n); // 创建一个新对象 o 并设置其原型为 n
o.name = "Mark"; // 设置 o 的 name 属性为 Mark
console.log(o.greeting()); // 调用 greeting 方法返回 "Hi, I'm Mark"
六十二、Object.seal
和 Object.freeze
方法有什么区别?
Object.freeze
和 Object.seal
的区别在于,当我们使用 Object.freeze
方法时,该对象的属性变得不可变,即我们不能改变或编辑这些属性的值。而在使用 Object.seal
方法时,我们可以改变现有的属性,但不能向对象添加新的属性。
六十三、in
操作符和 hasOwnProperty
方法的区别是什么?
这两个特性都用于检查一个对象中是否存在某个属性。它们都会返回 true
如果找到属性。不同之处在于 in
操作符还会检查对象的原型链中的属性,而 hasOwnProperty
方法只检查当前对象自身是否具有该属性,忽略原型链中的属性。
const o = {
prop: "bwahahah",
prop2: "hweasa"
};
console.log("prop" in o); // logs true
console.log("toString" in o); // logs true, 因为 toString 方法存在于对象的原型链中
console.log(o.hasOwnProperty("prop")); // logs true
console.log(o.hasOwnProperty("toString")); // logs false, 不检查对象的原型
六十四、处理 JavaScript 异步代码的方法有哪些?
处理异步代码的方式包括:
-
回调函数 -
Promise -
async/await -
第三方库如 async.js, bluebird, q, co
六十五、函数表达式和函数声明有何不同?
假设我们有以下例子:
hoistedFunc(); // 正常调用
notHoistedFunc(); // 抛出错误
function hoistedFunc() { // 函数声明
console.log("I am hoisted"); // 打印 "I am hoisted"
}
var notHoistedFunc = function() { // 函数表达式
console.log("I will not be hoisted!"); // 打印 "I will not be hoisted!"
}
notHoistedFunc()
的调用会抛出错误,而 hoistedFunc()
的调用不会,这是因为 hoistedFunc
是提升的,而 notHoistedFunc
则不是。
六十六、函数有多少种调用方式?
JavaScript 中函数可以有四种调用方式:
-
作为普通函数调用 - 如果函数不是作为一个方法、构造函数或使用 apply/call
调用,则作为普通函数调用,此时this
指向全局对象(浏览器环境下为window
)。function add(a, b) {
console.log(this); // 打印 window 对象
return a + b; // 返回 a+b 的结果
}
add(1, 5); // 打印 window 对象并返回 6
const o = {
method(callback) {
callback(); // 调用传入的回调函数
}
}
o.method(function () {
console.log(this); // 打印 window 对象
}); -
作为方法调用 - 当对象的一个属性值为函数时,这个函数被称为方法。调用方法时, this
的值将是拥有该方法的对象。const details = {
name: "Marko",
getName() { // 方法
return this.name; // 返回 this.name 的值
}
}
details.getName(); // 返回 Marko -
作为构造函数调用 - 如果函数通过 new
关键字调用,则称为构造函数。此时会创建一个新的空对象,并且this
指向这个新对象。function Employee(name, position, yearHired) {
this.name = name; // 设置 this.name 属性
this.position = position; // 设置 this.position 属性
this.yearHired = yearHired; // 设置 this.yearHired 属性
}
const emp = new Employee("Marko Polo", "Software Developer", 2017); // 创建 Employee 实例 -
使用 apply
和call
方法调用 - 如果想要显式地指定函数的this
值,可以使用apply
和call
方法。const obj1 = {
result: 0
};
const obj2 = {
result: 0
};
function reduceAdd() {
let result = 0;
for (let i = 0, len = arguments.length; i < len; i++) {
result += arguments[i]; // 累加 arguments 中的所有元素
}
this.result = result; // 设置 this.result 属性
}
reduceAdd.apply(obj1, [1, 2, 3, 4, 5]); // 将 obj1 作为 this
reduceAdd.call(obj2, 1, 2, 3, 4, 5); // 将 obj2 作为 this
六十七、什么是记忆化(Memoization)以及它的用途是什么?
记忆化是一种构建函数的技术,这种函数能够记住它之前计算过的结果。这样做的好处是可以避免重复计算相同输入的结果,节省了时间,但缺点是需要消耗更多内存来存储之前的计算结果。
六十八、实现一个记忆化辅助函数
function memoize(fn) {
const cache = {}; // 创建缓存对象
return function (param) {
if (cache[param]) { // 如果缓存中有结果
console.log('cached'); // 打印 "cached"
return cache[param]; // 返回缓存中的结果
} else {
let result = fn(param); // 计算结果
cache[param] = result; // 存储结果到缓存中
console.log(`not cached`); // 打印 "not cached"
return result; // 返回结果
}
}
}
const toUpper = (str = "") => str.toUpperCase(); // 转换字符串为大写
const toUpperMemoized = memoize(toUpper); // 使用记忆化包装 toUpper 函数
toUpperMemoized("abcdef"); // 计算 "abcdef" 的大写形式
toUpperMemoized("abcdef"); // 从缓存中获取 "abcdef" 的大写形式
为了支持多参数的情况,我们需要修改记忆化函数:
const slice = Array.prototype.slice;
function memoize(fn) {
const cache = {}; // 创建缓存对象
return (...args) => { // 接受任意数量的参数
const params = slice.call(args); // 获取参数列表
console.log(params); // 打印参数列表
if (cache[params]) { // 如果缓存中有结果
console.log('cached'); // 打印 "cached"
return cache[params]; // 返回缓存中的结果
} else {
let result = fn(...args); // 计算结果
cache[params] = result; // 存储结果到缓存中
console.log(`not cached`); // 打印 "not cached"
return result; // 返回结果
}
}
}
const makeFullName = (fName, lName) => `${fName} ${lName}`; // 生成全名
const reduceAdd = (numbers, startingValue = 0) => numbers.reduce((total, cur) => total + cur, startingValue); // 累加数组中的数字
const memoizedMakeFullName = memoize(makeFullName); // 使用记忆化包装 makeFullName 函数
const memoizedReduceAdd = memoize(reduceAdd); // 使用记忆化包装 reduceAdd 函数
memoizedMakeFullName("Marko", "Polo"); // 计算全名
memoizedMakeFullName("Marko", "Polo"); // 从缓存中获取全名
memoizedReduceAdd([1, 2, 3, 4, 5], 5); // 计算累加结果
memoizedReduceAdd([1, 2, 3, 4, 5], 5); // 从缓存中获取累加结果
六十九、为什么 typeof null
返回 'object'
?如何检查一个值是否为 null
?
typeof null
总是返回 'object'
,这是因为这是 JavaScript 自诞生以来的实现。曾经有人提议将其改为 'null'
,但由于这会导致现有项目和软件中的更多 Bug,因此被拒绝了。
我们可以使用严格等号 ===
来检查一个值是否为 null
:
function isNull(value) {
return value === null; // 检查值是否为 null
}
七十、new
关键字的作用是什么?
new
关键字与构造函数一起使用来创建 JavaScript 中的对象。
假设我们有以下示例代码:
function Employee(name, position, yearHired) {
this.name = name; // 设置 this.name 属性
this.position = position; // 设置 this.position 属性
this.yearHired = yearHired; // 设置 this.yearHired 属性
};
const emp = new Employee("Marko Polo", "Software Developer", 2017); // 创建 Employee 实例
new
关键字做了四件事:
-
创建一个空对象。 -
将空对象赋值给 this
。 -
函数将继承自 Employee.prototype
。 -
如果没有明确的 return
语句,则隐式返回this
。
在上面的例子中,它首先创建一个空对象 {}
, 然后将 this
的值指向这个空对象,并向其中添加属性。因为我们没有明确的 return
语句,它自动为我们返回了 this
。