48. 什么是Set
对象,它是如何工作的?
Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
我们可以使用Set
构造函数创建Set
实例。
const set1 = new Set(); const set2 = new Set(["a","b","c","d","d","e"]);
我们可以使用add
方法向Set
实例中添加一个新值,因为add
方法返回Set
对象,所以我们可以以链式的方式再次使用add
。如果一个值已经存在于Set
对象中,那么它将不再被添加。
set2.add("f"); set2.add("g").add("h").add("i").add("j").add("k").add("k"); // 后一个“k”不会被添加到set对象中,因为它已经存在了
我们可以使用has
方法检查Set
实例中是否存在特定的值。
set2.has("a") // true set2.has("z") // true
我们可以使用size
属性获得Set
实例的长度。
set2.size // returns 10
可以使用clear
方法删除 Set
中的数据。
set2.clear();
我们可以使用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]
49. 什么是回调函数?
回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)代码。
在JavaScript中函数也是对象的一种,同样对象可以作为参数传递给函数,因此函数也可以作为参数传递给另外一个函数,这个作为参数的函数就是回调函数。
const btnAdd = document.getElementById('btnAdd'); btnAdd.addEventListener('click', function clickCallback(e) { // do something useless });
在本例中,我们等待id
为btnAdd
的元素中的click
事件,如果它被单击,则执行clickCallback
函数。回调函数向某些数据或事件添加一些功能。
数组中的reduce
、filter
和map
方法需要一个回调作为参数。回调的一个很好的类比是,当你打电话给某人,如果他们不接,你留下一条消息,你期待他们回调。调用某人或留下消息的行为是事件或数据,回调是你希望稍后发生的操作。
50. Promise 是什么?
Promise 是异步编程的一种解决方案:从语法上讲,promise
是一个对象,从它可以获取异步操作的消息;从本意上讲,它是承诺,承诺它过一段时间会给你一个结果。promise
有三种状态:pending(等待态)
,fulfiled(成功态)
,rejected(失败态)
;状态一旦改变,就不会再变。创造promise
实例后,它会立即执行。
fs.readFile('somefile.txt', function (e, data) { if (e) { console.log(e); } console.log(data); });
如果我们在回调内部有另一个异步操作,则此方法存在问题。我们将有一个混乱且不可读的代码。此代码称为“回调地狱”。
// 回调地狱 fs.readFile('somefile.txt', function (e, data) { //your code here fs.readdir('directory', function (e, files) { //your code here fs.mkdir('directory', function (e) { //your code here }) }) })
如果我们在这段代码中使用promise
,它将更易于阅读、理解和维护。
promReadFile('file/path') .then(data => { return promReaddir('directory'); }) .then(data => { return promMkdir('directory'); }) .catch(e => { console.log(e); })
promise
有三种不同的状态:
- pending:初始状态,完成或失败状态的前一个状态
- fulfilled:操作成功完成
- rejected:操作失败
pending
状态的 Promise
对象会触发 fulfilled/rejected
状态,在其状态处理方法中可以传入参数/失败信息。当操作成功完成时,Promise 对象的 then
方法就会被调用;否则就会触发 catch
。如:
const myFirstPromise = new Promise((resolve, reject) => { setTimeout(function(){ resolve("成功!"); }, 250); }); myFirstPromise.then((data) => { console.log("Yay! " + data); }).catch((e) => {...});
51. 什么是 async/await
及其如何工作?
async/await
是 JS 中编写异步或非阻塞代码的新方法。它建立在Promises之上,让异步代码的可读性和简洁度都更高。
async/await
是 JS 中编写异步或非阻塞代码的新方法。它建立在Promises
之上,相对于 Promise 和回调,它的可读性和简洁度都更高。但是,在使用此功能之前,我们必须先学习Promises
的基础知识,因为正如我之前所说,它是基于Promise
构建的,这意味着幕后使用仍然是Promise。
使用 Promise
function callApi() { return fetch("url/to/api/endpoint") .then(resp => resp.json()) .then(data => { //do something with "data" }).catch(err => { //do something with "err" }); }
使用async/await
在async/await
,我们使用 tru/catch 语法来捕获异常。
async function callApi() { try { const resp = await fetch("url/to/api/endpoint"); const data = await resp.json(); //do something with "data" } catch (e) { //do something with "err" } }
注意:使用 async
关键声明函数会隐式返回一个Promise。
const giveMeOne = async () => 1; giveMeOne() .then((num) => { console.log(num); // logs 1 });
注意:await
关键字只能在async function
中使用。在任何非async function的函数中使用await
关键字都会抛出错误。await
关键字在执行下一行代码之前等待右侧表达式(可能是一个Promise)返回。
const giveMeOne = async () => 1; function getOne() { try { const num = await giveMeOne(); console.log(num); } catch (e) { console.log(e); } } // Uncaught SyntaxError: await is only valid in async function async function getTwo() { try { const num1 = await giveMeOne(); // 这行会等待右侧表达式执行完成 const num2 = await giveMeOne(); return num1 + num2; } catch (e) { console.log(e); } } await getTwo(); // 2
52. 展开(spread )运算符和 剩余(Rest) 运算符有什么区别?
展开运算符(spread)是三个点(...
),可以将一个数组转为用逗号分隔的参数序列。说的通俗易懂点,有点像化骨绵掌,把一个大元素给打散成一个个单独的小元素。
剩余运算符也是用三个点(...
)表示,它的样子看起来和展开操作符一样,但是它是用于解构数组和对象。在某种程度上,剩余元素和展开元素相反,展开元素会“展开”数组变成多个元素,剩余元素会收集多个元素和“压缩”成一个单一的元素。
function add(a, b) { return a + b; }; const nums = [5, 6]; const sum = add(...nums); console.log(sum);
在本例中,我们在调用add
函数时使用了展开操作符,对nums
数组进行展开。所以参数a
的值是5
,b
的值是6
,所以sum
是11
。
function add(...rest) { return rest.reduce((total,current) => total + current); }; 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]
这里,我们使用剩余操作符提取所有剩余的数组值,并将它们放入除第一项之外的其他数组中。
53. 什么是默认参数?
默认参数是在 JS 中定义默认变量的一种新方法,它在ES6或ECMAScript 2015版本中可用。
//ES5 Version function add(a,b){ a = a || 0; b = b || 0; return a + b; } //ES6 Version function add(a = 0, b = 0){ return a + b; } add(1); // returns 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"
54. 什么是包装对象(wrapper object)?
我们现在复习一下JS的数据类型,JS数据类型被分为两大类,基本类型和引用类型。
基本类型:Undefined
,Null
,Boolean
,Number
,String
,Symbol
,BigInt
引用类型:Object
,Array
,Date
,RegExp
等,说白了就是对象。
其中引用类型有方法和属性,但是基本类型是没有的,但我们经常会看到下面的代码:
let name = "marko"; console.log(typeof name); // "string" console.log(name.toUpperCase()); // "MARKO"
name
类型是 string
,属于基本类型,所以它没有属性和方法,但是在这个例子中,我们调用了一个toUpperCase()
方法,它不会抛出错误,还返回了对象的变量值。
原因是基本类型的值被临时转换或强制转换为对象,因此name
变量的行为类似于对象。除null
和undefined
之外的每个基本类型都有自己包装对象。也就是:String
,Number
,Boolean
,Symbol
和BigInt
。在这种情况下,name.toUpperCase()
在幕后看起来如下:
console.log(new String(name).toUpperCase()); // "MARKO"
在完成访问属性或调用方法之后,新创建的对象将立即被丢弃。
55. 隐式和显式转换有什么区别)?
隐式强制转换是一种将值转换为另一种类型的方法,这个过程是自动完成的,无需我们手动操作。
假设我们下面有一个例子。
console.log(1 + '6'); // 16 console.log(false + true); // 1 console.log(6 * '2'); // 12
第一个console.log
语句结果为16
。在其他语言中,这会抛出编译时错误,但在 JS 中,1
被转换成字符串,然后与+运
算符连接。我们没有做任何事情,它是由 JS 自动完成。
第二个console.log
语句结果为1
,JS 将false
转换为boolean
值为 0
,,true
为1
,因此结果为1
。
第三个console.log
语句结果12
,它将'2'
转换为一个数字,然后乘以6 * 2
,结果是12。
而显式强制是将值转换为另一种类型的方法,我们需要手动转换。
console.log(1 + parseInt('6'));
在本例中,我们使用parseInt
函数将'6'
转换为number
,然后使用+
运算符将1
和6
相加。
56. 什么是NaN?以及如何检查值是否为NaN?
NaN
表示“非数字”是 JS 中的一个值,该值是将数字转换或执行为非数字值的运算结果,因此结果为NaN
。
let a; console.log(parseInt('abc')); // NaN console.log(parseInt(null)); // NaN console.log(parseInt(undefined)); // NaN console.log(parseInt(++a)); // NaN console.log(parseInt({} * 10)); // NaN console.log(parseInt('abc' - 2)); // NaN console.log(parseInt(0 / 0)); // NaN console.log(parseInt('10a' * 10)); // NaN
JS 有一个内置的isNaN
方法,用于测试值是否为isNaN值,但是这个函数有一个奇怪的行为。
console.log(isNaN()); // true console.log(isNaN(undefined)); // true console.log(isNaN({})); // true console.log(isNaN(String('a'))); // true console.log(isNaN(() => { })); // true
所有这些console.log
语句都返回true
,即使我们传递的值不是NaN
。
在ES6
中,建议使用Number.isNaN
方法,因为它确实会检查该值(如果确实是NaN
),或者我们可以使自己的辅助函数检查此问题,因为在 JS 中,NaN是唯一的值,它不等于自己。
function checkIfNaN(value) { return value !== value; }
57. 如何判断值是否为数组?
我们可以使用Array.isArray
方法来检查值是否为数组。当传递给它的参数是数组时,它返回true
,否则返回false
。
console.log(Array.isArray(5)); // false console.log(Array.isArray("")); // false console.log(Array.isArray()); // false console.log(Array.isArray(null)); // false console.log(Array.isArray({ length: 5 })); // false console.log(Array.isArray([])); // true
如果环境不支持此方法,则可以使用polyfill
实现。
function isArray(value){ return Object.prototype.toString.call(value) === "[object Array]" }
当然还可以使用传统的方法:
let a = [] if (a instanceof Array) { console.log('是数组') } else { console.log('非数组') }
58. 如何在不使用%
模运算符的情况下检查一个数字是否是偶数?
我们可以对这个问题使用按位&
运算符,&
对其操作数进行运算,并将其视为二进制值,然后执行与运算。
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 | 1 | 1 |
因此,当我们执行console.log(5&1)
这个表达式时,结果为1
。首先,&
运算符将两个数字都转换为二进制,因此5
变为101
,1
变为001
。
然后,它使用按位怀运算符比较每个位(0
和1
)。 101&001
,从表中可以看出,如果a & b
为1
,所以5&1
结果为1
。
101 & 001 |
101 |
001 |
001 |
- 首先我们比较最左边的
1&0
,结果是0
。 - 然后我们比较中间的
0&0
,结果是0
。 - 然后我们比较最后
1&1
,结果是1
。 - 最后,得到一个二进制数
001
,对应的十进制数,即1
。
由此我们也可以算出console.log(4 & 1)
结果为0
。知道4
的最后一位是0
,而0 & 1
将是0
。如果你很难理解这一点,我们可以使用递归函数来解决此问题。
function isEven(num) { if (num < 0 || num === 1) return false; if (num == 0) return true; return isEven(num - 2); }
59. 如何检查对象中是否存在某个属性?
检查对象中是否存在属性有三种方法。
第一种使用 in
操作符号:
const o = { "prop" : "bwahahah", "prop2" : "hweasa" }; console.log("prop" in o); // true console.log("prop1" in o); // false
第二种使用 hasOwnProperty
方法,hasOwnProperty()
方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。
console.log(o.hasOwnProperty("prop2")); // true console.log(o.hasOwnProperty("prop1")); // false
第三种使用括号符号obj["prop"]
。如果属性存在,它将返回该属性的值,否则将返回undefined
。
console.log(o["prop"]); // "bwahahah" console.log(o["prop1"]); // undefined
60. AJAX 是什么?
即异步的 JavaScript 和 XML,是一种用于创建快速动态网页的技术,传统的网页(不使用 AJAX)如果需要更新内容,必需重载整个网页面。使用AJAX则不需要加载更新整个网页,实现部分内容更新
用到AJAX的技术:
- HTML - 网页结构
- CSS - 网页的样式
- JavaScript - 操作网页的行为和更新DOM
- XMLHttpRequest API - 用于从服务器发送和获取数据
- PHP,Python,Nodejs - 某些服务器端语言
61. 如何在 JS 中创建对象?
使用对象字面量:
const o = { name: "Mark", greeting() { return `Hi, I'm ${this.name}`; } }; o.greeting(); //returns "Hi, I'm Mark"
使用构造函数:
function Person(name) { this.name = name; } Person.prototype.greeting = function () { return `Hi, I'm ${this.name}`; } const mark = new Person("Mark"); mark.greeting(); //returns "Hi, I'm Mark"
使用 Object.create 方法:
const n = { greeting() { return `Hi, I'm ${this.name}`; } }; const o = Object.create(n); // sets the prototype of "o" to be "n" o.name = "Mark"; console.log(o.greeting()); // logs "Hi, I'm Mark"
62. Object.seal 和 Object.freeze 方法之间有什么区别?
这两种方法之间的区别在于,当我们对一个对象使用Object.freeze方法时,该对象的属性是不可变的,这意味着我们不能更改或编辑这些属性的值。而在Obj.Engor方法中,我们可以改变现有的属性。
Object.freeze()
Object.freeze()
方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze()
返回和传入的参数相同的对象。
Object.seal()
Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。
方法的相同点:
- ES5新增。
- 对象不可能扩展,也就是不能再添加新的属性或者方法。
- 对象已有属性不允许被删除。
- 对象属性特性不可以重新配置。
方法不同点:
Object.seal
方法生成的密封对象,如果属性是可写的,那么可以修改属性值。Object.freeze
方法生成的冻结对象,属性都是不可写的,也就是属性值无法更改。
63. in
运算符和 Object.hasOwnProperty
方法有什么区别?
如你所知,这两个特性都检查对象中是否存在属性,它将返回truefalse。它们之间的区别在于,in操作符还会检查对象的原型链,如果属性在当前对象中没有找到,而hasOwnProperty方法只检查属性是否存在于当前对象中,而忽略原型链。
hasOwnPropert方法
hasOwnPropert()
方法返回值是一个布尔值,指示对象自身属性中是否具有指定的属性,因此这个方法会忽略掉那些从原型链上继承到的属性。
看下面的例子:
Object.prototype.phone= '15345025546'; let obj = { name: '西门大官人', age: '28' } console.log(obj.hasOwnProperty('phone')) // false console.log(obj.hasOwnProperty('name')) // true
可以看到,如果在函数原型上定义一个变量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("注意:我没有被提升"); }
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" 对象 });
- 作为方法调用——如果一个对象的属性有一个函数的值,我们就称它为方法。调用该方法时,该方法的
this
值指向该对象。
const details = { name : "Marko", getName(){ return this.name; } } details.getName(); // Marko // the "this" value inside "getName" method will be the "details" object
- 作为构造函数的调用-如果在函数之前使用
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);
- 使用
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
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");
这个缓存函数适用于接受一个参数。我们需要改变下,让它接受多个参数。
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);
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; }
70. new 关键字有什么作用?
new
关键字与构造函数一起使用以创建对象在JavaScript中。
下面看看例子:
function Employee(name, position, yearHired) { this.name = name; this.position = position; this.yearHired = yearHired; }; const emp = new Employee("Marko Polo", "Software Developer", 2017);
new
关键字做了4
件事:
- 创建空对象
{}
- 将空对象分配给
this
值 - 将空对象的
__proto__
指向构造函数的prototype
- 如果没有使用显式
return
语句,则返回this
根据上面描述的,它将首先创建一个空对象{},然后它将this值赋给这个空对象this={},并向这个对象添加属性。因为我们没有显式的return语句,所以它会自动为我们返回this。
为了保证的可读性,本文采用意译而非直译。