1. null和undefined的区别?
在理解undefined和null的区别之前,先来看看它们的相似类型:
它们属于JavaScript的7种基本类型。
- string
- number
- boolean
- null
- undefined
- symbol
- bigint
它们是属于虚值,使用Boolean(value)或!!value将其转换为布尔值时,值为false。
console.log(!!null); // false
console.log(!!undefined); // false
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
2
3
4
5
接着来看看它们的区别: undefined是未指定特定值的变量的默认值,或者没有显式返回值的函数,如:console.log(1),还包括对象中不存在的属性,这些JS引擎都会为其分配 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
2
3
4
5
6
7
8
9
10
11
null是“不代表任何值的值”。null是已明确定义给变量的值。在此示例中,当fs.readFile方法未引发错误时,我们将获得null值。
fs.readFile('path/to/file', (e,data) => {
console.log(e); // 当没有错误发生时,打印 null
if(e){
console.log(e);
}
console.log(data);
});
2
3
4
5
6
7
在比较null和undefined时,我们使用==
时得到true,使用===
时得到false:
console.log(null == undefined); // true
console.log(null === undefined); // false
2
2. &&运算符能做什么
&&即逻辑与,在其操作数中找到第一个虚值表达式并返回它,如果没有找到任何虚值表达式,则返回最后一个真值表达式。它采用短路来防止不必要的工作。
console.log(false && 1 && []); // false
console.log(" " && true && 5); // 5
2
使用if语句:
const router: Router = Router();
router.get('/endpoint', (req: Request, res: Response) => {
let conMobile: PoolConnection;
try {
//do some db operations
} catch (e) {
if (conMobile) {
conMobile.release();
}
}
});
2
3
4
5
6
7
8
9
10
11
12
使用&&操作符:
const router: Router = Router();
router.get('/endpoint', (req: Request, res: Response) => {
let conMobile: PoolConnection;
try {
//do some db operations
} catch (e) {
conMobile && conMobile.release()
}
});
2
3
4
5
6
7
8
9
10
3. ||运算符能做什么
||即逻辑或,在其操作数中找到第一个真值表达式并返回它。这也使用了短路来防止不必要的工作。在支持ES6默认函数参数之前,它用于初始化函数中的默认参数值。
console.log(null || 1 || undefined); // 1
function logName(name) {
var n = name || "Mark";
console.log(n);
}
logName(); // "Mark"
2
3
4
5
6
7
8
+
或一元加运算符是将字符串转换为数字的最快方法吗?
4. 使用根据MDN文档,+是将字符串转换为数字的最快方法,因为如果值已经是数字,它不会执行任何操作。
5. 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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
等价的DOM是这样的: JS 中的document对象表示DOM。它为我们提供了许多方法,我们可以使用这些方法来选择元素来更新元素内容,等等。
6. 什么是事件传播?
当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。在事件“冒泡阶段”中,事件冒泡或向上传播至父级(可能存在多层父级),直到到达window为止;而在“捕获阶段”中,事件从window开始向下触发元素事件或event.target
。
事件传播有三个阶段:
- 捕获阶段:事件从window开始,然后向下到每个元素,直到到达目标元素。
- 目标阶段:事件已达到目标元素。
- 冒泡阶段:事件从目标元素冒泡,然后上升到每个元素,直到到达window。
7. 谈谈事件冒泡
当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。在冒泡阶段,事件冒泡,或者事件发生在它的父代,祖父母,祖父母的父代,直到到达window为止。
假设有如下的HTML结构:
<div class="grandparent">
<div class="parent">
<div class="child">1</div>
</div>
</div>
2
3
4
5
对应的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); // isCapture 标识是否为事件捕获,默认为false,即事件冒泡
}
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');
})
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
addEventListener方法具有第三个可选参数useCapture,其默认值为false,事件将在冒泡阶段中发生,如果为true,则事件将在捕获阶段中发生。
如果单击child元素,它将分别在控制台上记录child,parent,grandparent,html,document和window,这就是事件冒泡。
8. 谈谈事件捕获
当事件发生在 DOM 元素上时,该事件并不完全发生在那个元素上。在捕获阶段,事件从window开始,一直到触发事件的元素。
假设有如下的 HTML 结构:
<div class="grandparent">
<div class="parent">
<div class="child">1</div>
</div>
</div>
2
3
4
5
对应的JS代码:
function addEvent(el, event, callback, isCapture = true) {
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');
})
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
如果单击child元素,它将分别在控制台上打印window,document,html,grandparent和parent,这就是事件捕获。
9. event.preventDefault()和event.stopPropagation()方法之间有什么区别?
- event.preventDefault()方法作用是阻止元素的默认行为。如果在表单元素中使用,它将阻止其提交。如果在锚元素中使用,它将阻止其导航。如果在上下文菜单中使用,它将阻止其显示或显示。
- event.stopPropagation()方法作用是阻止捕获和冒泡阶段中当前事件的进一步传播。
- 如何知道是否在元素中使用了
event.preventDefault()
方法? 我们可以在事件对象中使用event.defaultPrevented属性。它返回一个布尔值用来表明是否在特定元素中调用了event.preventDefault()。
obj.someprop.x
会引发错误?
11. 为什么此代码const obj = {};
console.log(obj.someprop.x);
2
显然,由于我们尝试访问someprop属性中的x属性,而someprop并没有在对象中,所以值为undefined。记住对象本身不存在的属性,并且其原型的默认值为undefined。因为undefined没有属性x,所以试图访问将会报错。
12. 什么是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>
2
3
4
5
6
7
8
9
10
JS代码如下:
function clickFunc(event) {
console.log(event.target);
}
2
3
如果单击button,即使我们将事件附加在最外面的div上,它也将打印button标签,因此我们可以得出结论event.target是触发事件的元素。
13. 什么是 event.currentTarget?
event.currentTarget是我们在其上显式添加事件处理程序的元素(在下述例子中即最外层的div)。
假设有如下的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>
2
3
4
5
6
7
8
9
10
JS代码如下:
function clickFunc(event) {
console.log(event.currentTarget);
}
2
3
如果单击button,即使我们单击该button,它也会打印最外面的div标签。在此示例中,我们可以得出结论,event.currentTarget是添加事件处理程序的元素。
==
和===
有什么区别?
14. ==
用于一般比较,===
用于严格比较,==
在比较的时候可以转换数据类型,===
严格比较,只要类型不匹配就返回flase。
先来看==
:
强制是将值转换为另一种类型的过程。在这种情况下,==
会执行隐式强制。在比较两个值之前,==
需要执行一些规则。
假设我们要比较x == y的值。
- 如果x和y的类型相同,则js会换成===操作符进行比较。
- 如果x为null, y为undefined,则返回true;同理,如果x为undefined且y为null,则返回true。
- 如果x和y一个是number,另一个是string,即将字符串转为number再比较。比如:x的类型是number,y的类型是string,那么返回x == Number(y)。
- 如果x和y有一个类型是boolean,比如x为类型是boolean,则返回Number(x) == y。
- 如果x是string、symbol或number,而y是object类型,则返回x == toPrimitive(y)。
- 如果x是object,y是string,symbol,则返回toPrimitive(x) == y。
- 剩下的返回false
注意:toPrimitive
首先在对象中使用valueOf方法,然后使用toString方法来获取该对象的原始值。
来看一些例子:
x | y | x == y |
---|---|---|
5 | 5 | true |
1 | '1' | true |
null | undefiend | true |
0 | false | true |
'1,2' | [1, 2] | true |
'[object Object]' | {} | true |
- 第一个示例符合条件1,因为x和y具有相同的类型和值。
- 第二个示例符合条件3,在比较之前将y转换为数字。
- 第三个例子符合条件2。
- 第四个例子符合条件4,因为y是boolean类型。
- 第五个示例符合条件5。使用toString()方法将数组转换为字符串,该方法返回1,2。
- 最后一个示例符合条件6。使用toString()方法将对象转换为字符串,该方法返回[object Object]。
x | y | x === y |
---|---|---|
5 | 5 | true |
1 | '1' | false |
null | undefiend | false |
0 | false | false |
'1,2' | [1, 2] | false |
'[object Object]' | {} | false |
如果使用===运算符,则第一个示例以外的所有比较将返回false,因为它们的类型不同,而第一个示例将返回true,因为两者的类型和值相同。
15. 为什么在JS中比较两个相似的对象时返回false?
先看下面的例子:
let a = { a: 1 };
let b = { a: 1 };
let c = a;
// a和b分别指向两个不同对象的堆内存地址,所以两者不相等
// 一定要清楚变量a/b/c中存储的是对象的内存地址值
console.log(a === b); // 打印 false,即使它们有相同的属性
// a和c指向同一个对象的堆内存地址,因此相等
console.log(a === c); // true
2
3
4
5
6
7
8
9
16. !! 运算符能做什么?
!!运算符可以将右侧的值强制转换为布尔值,这也是将值转换为布尔值的一种简单方法。
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
2
3
4
5
6
7
8
9
10
17. 如何在一行中计算多个表达式的值?
可以使用逗号运算符在一行中计算多个表达式。它从左到右求值,并返回右边最后一个项目或最后一个操作数的值。
let x = 5;
x = (x++ , x = addFive(x), x *= 2, x -= 5, x += 10);
function addFive(num) {
return num + 5;
}
2
3
4
5
6
7
上面的结果最后得到x的值为27。首先,我们将x的值增加到6,然后调用函数addFive(6)并将6作为参数传递并将结果重新分配给x,此时x的值为11。之后,将x的当前值乘以2并将其分配给x,x的更新值为22。然后,将x的当前值减去5并将结果分配给x x更新后的值为17。最后,我们将x的值增加10,然后将更新的值分配给x,最终x的值为27。
18. 什么是提升?
提升是用来描述变量和函数移动到其(全局或函数)作用域顶部的术语。
为了理解提升,需要来了解一下执行上下文。执行上下文是当前正在执行的“代码环境”。执行上下文有两个阶段:编译和执行。
- 编译阶段,JS引荐获取所有函数声明并将其提升到其作用域的顶部,以便我们稍后可以引用它们,同时获取所有变量声明(使用var关键字进行声明),还会为它们提供默认值undefined。
- 执行阶段,它将值赋给之前提升的变量,并执行或调用函数(对象中的方法)。
注意:只有使用var声明的变量,或者函数声明才会被提升,相反,函数表达式(会发生变量声明提升)或箭头函数,let和const声明的变量,这些都不会被提升。
假设在全局使用域,有如下的代码:
console.log(y); // undefined
y = 1;
console.log(y); // 1
console.log(greet('lisi')); // hello lisi
function greet(name) { // 发生函数声明提升
return 'hello ' + name;
}
var y; // 发生变量声明提升
2
3
4
5
6
7
8
9
10
上面代码在编译阶段其实是这样的:
function greet(name) {
return 'hello ' + name;
}
var y; // 默认值 undefined
// 等待“编译”阶段完成,然后开始“执行”阶段
/*
console.log(y);
y = 1;
console.log(y);
console.log(greet('lisi'));
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
编译阶段完成后,它将启动执行阶段调用方法,并将值分配给变量。
function greet(name) {
return 'Hello ' + name;
}
var y;
// start "execution" phase
console.log(y);
y = 1;
console.log(y);
console.log(greet('lisi'));
2
3
4
5
6
7
8
9
10
11
12
19. 什么是作用域?
JavaScript中的作用域是我们可以有效访问变量或函数的区域。JS有三种类型的作用域:
- 全局作用域
- 函数作用域
- 块级作用域(ES6)
全局作用域:在全局命名空间中声明的变量或函数位于全局作用域中,因此在代码中的任何地方都可以访问它们。
// global namespace
var g = "global";
function globalFunc(){
function innerFunc(){
console.log(g); // can access "g" because "g" is a global variable
}
innerFunc();
}
2
3
4
5
6
7
8
9
函数作用域:在函数中声明的变量、函数和参数可以在函数内部访问,但不能在函数外部访问。
function myFavoriteFunc(a) { // a是函数形参,也属于myFavoriteFunc中的局部变量
if (true) {
var b = "Hello " + a; // b属于函数myFavoriteFunc作用域
}
return b;
}
myFavoriteFunc("World");
// 这里会报错,因为全局中并没有声明变量a
console.log(a); // Throws a ReferenceError "a" is not defined
// 上面就报错了,因此下述代码根本不会执行
console.log(b);
2
3
4
5
6
7
8
9
10
11
12
13
块级作用域:在块{}中声明的变量(let,const)只能在其中访问。
function testBlock() {
if(true) {
let z = 5;
}
return z; // 这里无法访问if块中的z
}
testBlock(); // Throws a ReferenceError "z" is not defined
2
3
4
5
6
7
8
作用域也是一组用于查找变量的规则。如果变量在当前作用域中不存在,它将向外部作用域中查找并搜索,如果该变量不存在,它将再次查找直到到达全局作用域,如果找到,则可以使用它,否则引发错误,这种查找过程也称为作用域链。
/* 作用域链
内部作用域->外部作用域-> 全局作用域
*/
// 全局作用域
var variable1 = "Comrades";
var variable2 = "Sayonara";
function outer() {
// 外部作用域
var variable1 = "World";
function inner() {
// 内部作用域
var variable2 = "Hello";
// 访问variable2,内部作用域有就直接使用,不会沿着作用域链向上查找
// variable1在内部作用域中没有,则沿着作用域链向上查找,找到则直接使用
// 如果找到全局中还没找到就会报错
console.log(variable2 + " " + variable1);
}
inner();
}
outer(); // Hello World
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
20. 什么是闭包?
闭包就是一个函数在声明时能够记住当前作用域、父函数作用域、及父函数作用域上的变量和参数的引用,直至通过作用域链上全局作用域,基本上闭包是在声明函数时创建的作用域。
来看个例子:
// 全局作用域
var globalVar = "abc";
function a() {
console.log(globalVar);
}
a(); // "abc"
2
3
4
5
6
7
8
在上述例子中,当我们声明a函数时,全局作用域是a闭包的一部分。 来看一个更复杂的例子:
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");
2
3
4
5
6
7
8
9
10
11
12
13
14
上面例子结果是guess outer inner
。
当我们调用outerFunc函数并将返回值innerFunc函数分配给变量x时,即使我们为outerVar变量分配了新值outer-2,outerParam也继续保留outer值,因为重新分配是在调用outerFunc之后发生的,并且当我们调用outerFunc函数时,它会在作用域链中查找outerVar的值,此时的outerVar的值将为"outer"(因为函数作用域是在函数定义的时候就已经确定了)。
现在,当我们调用引用了innerFunc的x变量时,innerParam将具有一个inner值,因为这是我们在调用中传递的值,而globalVar变量值为guess,因为在调用x变量之前,我们将一个新值分配给globalVar。
下面这个示例演示没有理解好闭包所犯的错误:
const arrFuncs = [];
for(var i = 0; i < 5; i++){
arrFuncs.push(function (){
return i;
});
}
console.log(i); // i is 5
for (let i = 0; i < arrFuncs.length; i++) {
console.log(arrFuncs[i]()); // 都打印 5
}
2
3
4
5
6
7
8
9
10
11
由于闭包,此代码无法正常运行。var关键字创建一个全局变量,当我们 push 一个函数时,这里返回的全局变量i。因此,当我们在循环后在该数组中调用其中一个函数时,它会打印5,因为我们得到i的当前值为5,我们可以访问它,因为它是全局变量。
因为闭包在创建变量时会保留该变量的引用而不是其值。我们可以使用IIFE或使用let来代替var的声明。
21. JavaScript中的虚值是什么?
const falsyValues = ['', 0, null, undefined, NaN, false];
简单的来说,虚值就是在类型转换为布尔值时变为false的值。
22. 如何检查值是否虚值?
使用Boolean函数或者!!运算符。
23. 'use strict' 是干嘛用的?
"use strict" 是 ES5 特性,它使我们的代码在函数或整个脚本中处于严格模式。严格模式帮助我们在代码的早期避免 bug,并为其添加限制。
严格模式的一些限制:
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用with语句
- 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
- eval不能在它的外层作用域引入变量
- eval和arguments不能被重新赋值
- arguments不会自动反映函数参数的变化
- 不能使用arguments.callee
- 不能使用arguments.caller
- 禁止this指向全局对象
- 不能使用fn.caller和fn.arguments获取函数调用的堆栈
- 增加了保留字(比如protected、static和interface)
设立”严格模式”的目的,主要有以下几个:
- 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
- 消除代码运行的一些不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的Javascript做好铺垫。
24. JavaScript中this值是什么?
62. Object.seal和Object.freeze方法之间有什么区别?
这两种方法之间的区别在于:当我们对一个对象使用Object.freeze方法时,该对象的属性是不可变的,这意味着我们不能更改或编辑这些属性的值。而在Obj.Engor方法中,我们可以改变现有的属性。
Object.freeze()方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。
Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。
方法的相同点:
- ES5新增。
- 对象不可能扩展,也就是不能再添加新的属性或者方法。
- 对象已有属性不允许被删除。
- 对象属性特性不可以重新配置。
方法不同点:
- Object.seal方法生成的密封对象,如果属性是可写的,那么可以修改属性值。
- Object.freeze方法生成的冻结对象,属性都是不可写的,也就是属性值无法更改。
63. in运算符和 Object.hasOwnProperty 方法有什么区别?
我们都知道,这两个特性都能检查对象中是否存在某个属性,它将返回true或者false。它们之间的区别在于:in操作符还会检查对象的原型链,如果属性在当前对象中没有找到,而hasOwnProperty方法只检查属性是否存在于当前对象中,而忽略原型链。
hasOwnPropert()方法返回值是一个布尔值,指示对象自身属性中是否具有指定的属性,因此这个方法会忽略掉那些从原型链上继承到的属性。
看下面的例子:
Object.prototype.phone= '13299094321';
let obj = {
name: 'lisi',
age: '28'
}
console.log(obj.hasOwnProperty('phone')) // false
console.log(obj.hasOwnProperty('name')) // true
2
3
4
5
6
7
8
可以看到,如果在函数原型上定义一个变量phone,hasOwnProperty方法会直接忽略掉。
in运算符
如果指定的属性在指定的对象或其原型链中,则in 运算符返回true。
还是用上面的例子来演示:
console.log('phone' in obj) // true
可以看到in运算符会检查它或者其原型链是否包含具有指定名称的属性。
64. 有哪些方法可以处理JS中的异步代码?
- 回调函数
- Promise
- async/await
- 还有一些库:async.js, bluebird, q, co
65. 函数表达式和函数声明之间有什么区别?
看下面的例子:
hoistedFunc();
notHoistedFunc();
function hoistedFunc(){
console.log("注意:我会被提升");
}
var notHoistedFunc = function(){
console.log("注意:我没有被提升");
}
2
3
4
5
6
7
8
9
10
notHoistedFunc调用抛出异常:Uncaught TypeError: notHoistedFunc is not a function,而hoistedFunc调用不会,因为hoistedFunc会被提升到作用域的顶部,而notHoistedFunc 不会。
66. 调用函数,可以使用哪些方法?
在JS中有4种方法可以调用函数:
作为函数调用——如果一个函数没有作为方法、构造函数、apply、call调用时,此时this指向的是window对象(非严格模式)
//Global Scope
function add(a,b){
console.log(this);
return a + b;
}
add(1,5); // 打印 "window" 对象和 6
const o = {
method(callback){
callback();
}
}
o.method(function (){
console.log(this); // 打印 "window" 对象
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
作为方法调用——如果一个对象的属性有一个函数的值,我们就称它为方法。调用该方法时,该方法的this值指向该对象。
const details = {
name : "Marko",
getName(){
return this.name;
}
}
details.getName(); // Marko
// the "this" value inside "getName" method will be the "details" object
2
3
4
5
6
7
8
9
作为构造函数的调用-如果在函数之前使用new关键字调用了函数,则该函数称为构造函数。构造函数里面会默认创建一个空对象,并将this指向该对象。
function Employee(name, position, yearHired) {
// creates an empty object {}
// then assigns the empty object to the "this" keyword
// this = {};
this.name = name;
this.position = position;
this.yearHired = yearHired;
// inherits from Employee.prototype
// returns the "this" value implicitly if no
// explicit return statement is specified
};
const emp = new Employee("Marko Polo", "Software Developer", 2017);
2
3
4
5
6
7
8
9
10
11
12
13
使用apply和call方法调用——如果我们想显式地指定一个函数的this值,我们可以使用这些方法,这些方法对所有函数都可用。
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]); // reduceAdd 函数中的 this 对象将是 obj1
reduceAdd.call(obj2, 1, 2, 3, 4, 5); // reduceAdd 函数中的 this 对象将是 obj2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
67. 什么是缓存及它有什么作用?
缓存是建立一个函数的过程,这个函数能够记住之前计算的结果或值。使用缓存函数是为了避免在最后一次使用相同参数的计算中已经执行的函数的计算。这节省了时间,但也有不利的一面,即我们将消耗更多的内存来保存以前的结果。
68. 手动实现缓存方法
function memoize(fn) {
const cache = {};
return function (param) {
if (cache[param]) {
console.log('cached');
return cache[param];
} else {
let result = fn(param);
cache[param] = result;
console.log(`not cached`);
return result;
}
}
}
const toUpper = (str ="")=> str.toUpperCase();
const toUpperMemoized = memoize(toUpper);
toUpperMemoized("abcdef");
toUpperMemoized("abcdef");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这个缓存函数适用于接受一个参数。我们需要改变下,让它接受多个参数。
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');
return cache[params];
} else {
let result = fn(...args);
cache[params] = result;
console.log(`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);
const memoizedReduceAdd = memoize(reduceAdd);
memoizedMakeFullName("Marko", "Polo");
memoizedMakeFullName("Marko", "Polo");
memoizedReduceAdd([1, 2, 3, 4, 5], 5);
memoizedReduceAdd([1, 2, 3, 4, 5], 5);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
69. 为什么typeof null 返回 object?如何检查一个值是否为 null?
typeof null == 'object'
总是返回true,因为这是自 JS 诞生以来null的实现。曾经有人提出将typeof null == 'object'
修改为typeof null == 'null'
,但是被拒绝了,因为这将导致更多的bug。
我们可以使用严格相等运算符===来检查值是否为null。
function isNull(value) {
return value === null;
}
2
3
70. new关键字有什么作用?
在JavaScript中,new关键字与构造函数一起使用用来创建对象。
来看个例子:
function Employee(name, position, yearHired) {
this.name = name;
this.position = position;
this.yearHired = yearHired;
};
const emp = new Employee("Marko Polo", "Software Developer", 2017);
2
3
4
5
6
7
new关键字做了4件事:
- 创建一个空对象{};
- 将构造函数中的this指向这个空对象;
- 将空对象的
__proto__
指向构造函数的prototype; - 如果没有使用显式return语句,则返回this(即实例对象)。
根据上面描述的,它将首先创建一个空对象{},然后它将this值赋给这个空对象this={},并向这个对象添加属性。因为我们没有显式的return语句,所以它会自动为我们返回this。