作者:Mark A 译者:前端小智 来源:dev
由于篇幅过长,我将此系列分成上中下三篇,上篇:
看完这几道 JavaScript 面试题,让你与考官对答如流(上)
-
26. 什么是IIFE,它的用途是什么?
-
27. Function.prototype.apply方法的用途是什么?
-
28. Function.prototype.call方法的用途是什么?
-
29. Function.prototype.apply 和 Function.prototype.call 之间有什么区别?
-
30. Function.prototype.bind的用途是什么?
-
31. 什么是函数式编程? JavaScript的哪些特性使其成为函数式语言的候选语言?
-
32. 什么是高阶函数?
-
33. 为什么函数被称为一等公民?
-
34. 手动实现`Array.prototype.map`方法
-
35. 手动实现`Array.prototype.filter`方法
-
35. 手动实现`Array.prototype.reduce`方法
-
37. arguments 的对象是什么?
-
38. 如何创建一个没有 prototype(原型) 的对象?
-
39. 为什么在调用这个函数时,代码中的`b`会变成一个全局变量?
-
40. ECMAScript是什么?
-
41. ES6或ECMAScript 2015有哪些新特性?
-
42. `var`,`let`和`const`的区别是什么
-
43. 什么是箭头函数?
-
44. 什么是类?
-
45. 什么是模板字符串?
-
46. 什么是对象解构?
-
47. 什么是 ES6 模块?
-
48. 什么是`Set`对象,它是如何工作的?
-
49. 什么是回调函数?
-
50. Promise 是什么?
IIFE或立即调用的函数表达式是在创建或声明后将被调用或执行的函数。创建IIFE的语法是,将function (){}
包裹在在括号()
内,然后再用另一个括号()
调用它,如:(function(){})()
(function(){
...
} ());
(function () {
...
})();
(function named(params) {
...
})();
(() => {
});
(function (global) {
...
})(window);
const utility = (function () {
return {
...
}
})
这些示例都是有效的IIFE。倒数第二个救命表明我们可以将参数传递给IIFE函数。最后一个示例表明,我们可以将IIFE
的结果保存到变量中,以便稍后使用。
IIFE的一个主要作用是避免与全局作用域内的其他变量命名冲突或污染全局命名空间,来个例子。
假设我们引入了一个omelibr.js
的链接,它提供了一些我们在代码中使用的全局函数,但是这个库有两个方法我们没有使用:createGraph
和drawGraph
,因为这些方法都有bug
。我们想实现自己的createGraph
和drawGraph
方法。
解决此问题的一种方法是直接覆盖:
function createGraph() {
// createGraph logic here
}
function drawGraph() {
// drawGraph logic here
}
当我们使用这个解决方案时,我们覆盖了库提供给我们的那两个方法。
另一种方式是我们自己改名称:
function myCreateGraph() {
// createGraph logic here
}
function myDrawGraph() {
// drawGraph logic here
}
当我们使用这个解决方案时,我们把那些函数调用更改为新的函数名。
还有一种方法就是使用IIFE:
const graphUtility = (function () {
function createGraph() {
// createGraph logic here
}
function drawGraph() {
// drawGraph logic here
}
return {
createGraph,
drawGraph
}
})
在此解决方案中,我们要声明了graphUtility
变量,用来保存IIFE执行的结果,该函数返回一个包含两个方法createGraph
和drawGraph
的对象。
IIFE 还可以用来解决一个常见的面试题:
var li = document.querySelectorAll('.list-group > li');
for (var i = 0, len = li.length; i li');
for (var i = 0, len = li.length; i 找不到c这个变量
const c = 10;
let
和const
声明形成块作用域
if(1){
var a = 100;
let b = 10;
}
console.log(a); // 100
console.log(b) // 报错:b is not defined ===> 找不到b这个变量
-------------------------------------------------------------
if(1){
var a = 100;
const c = 1;
}
console.log(a); // 100
console.log(c) // 报错:c is not defined ===> 找不到c这个变量
同一作用域下let
和const
不能声明同名变量,而var
可以
var a = 100;
console.log(a); // 100
var a = 10;
console.log(a); // 10
-------------------------------------
let a = 100;
let a = 10;
// 控制台报错:Identifier 'a' has already been declared ===> 标识符a已经被声明了。
暂存死区
var a = 100;
if(1){
a = 10;
//在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a,
// 而这时,还未到声明时候,所以控制台Error:a is not defined
let a = 1;
}
const
/*
* 1、一旦声明必须赋值,不能使用null占位。
*
* 2、声明后不能再修改
*
* 3、如果声明的是复合类型数据,可以修改其属性
*
* */
const a = 100;
const list = [];
list[0] = 10;
console.log(list); // [10]
const obj = {a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj); // {a:10000,name:'apple'}
43. 什么是箭头函数?
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this
,arguments
,super
或new.target
。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
//ES5 Version
var getCurrentDate = function (){
return new Date();
}
//ES6 Version
const getCurrentDate = () => new Date();
在本例中,ES5 版本中有function(){}
声明和return
关键字,这两个关键字分别是创建函数和返回值所需要的。在箭头函数版本中,我们只需要()
括号,不需要 return
语句,因为如果我们只有一个表达式或值需要返回,箭头函数就会有一个隐式的返回。
//ES5 Version
function greet(name) {
return 'Hello ' + name + '!';
}
//ES6 Version
const greet = (name) => `Hello ${name}`;
const greet2 = name => `Hello ${name}`;
我们还可以在箭头函数中使用与函数表达式和函数声明相同的参数。如果我们在一个箭头函数中有一个参数,则可以省略括号。
const getArgs = () => arguments
const getArgs2 = (...rest) => rest
箭头函数不能访问arguments
对象。所以调用第一个getArgs
函数会抛出一个错误。相反,我们可以使用rest参数来获得在箭头函数中传递的所有参数。
const data = {
result: 0,
nums: [1, 2, 3, 4, 5],
computeResult() {
// 这里的“this”指的是“data”对象
const addAll = () => {
return this.nums.reduce((total, cur) => total + cur, 0)
};
this.result = addAll();
}
};
箭头函数没有自己的this
值。它捕获词法作用域函数的this
值,在此示例中,addAll
函数将复制computeResult
方法中的this
值,如果我们在全局作用域声明箭头函数,则this
值为 window
对象。
类(class)
是在 JS 中编写构造函数的新方法。它是使用构造函数的语法糖,在底层中使用仍然是原型和基于原型的继承。
//ES5 Version
function Person(firstName, lastName, age, address){
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.address = address;
}
Person.self = function(){
return this;
}
Person.prototype.toString = function(){
return "[object Person]";
}
Person.prototype.getFullName = function (){
return this.firstName + " " + this.lastName;
}
//ES6 Version
class Person {
constructor(firstName, lastName, age, address){
this.lastName = lastName;
this.firstName = firstName;
this.age = age;
this.address = address;
}
static self() {
return this;
}
toString(){
return "[object Person]";
}
getFullName(){
return `${this.firstName} ${this.lastName}`;
}
}
重写方法并从另一个类继承。
//ES5 Version
Employee.prototype = Object.create(Person.prototype);
function Employee(firstName, lastName, age, address, jobTitle, yearStarted) {
Person.call(this, firstName, lastName, age, address);
this.jobTitle = jobTitle;
this.yearStarted = yearStarted;
}
Employee.prototype.describe = function () {
return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}`;
}
Employee.prototype.toString = function () {
return "[object Employee]";
}
//ES6 Version
class Employee extends Person { //Inherits from "Person" class
constructor(firstName, lastName, age, address, jobTitle, yearStarted) {
super(firstName, lastName, age, address);
this.jobTitle = jobTitle;
this.yearStarted = yearStarted;
}
describe() {
return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}`;
}
toString() { // Overriding the "toString" method of "Person"
return "[object Employee]";
}
}
所以我们要怎么知道它在内部使用原型?
class Something {
}
function AnotherSomething(){
}
const as = new AnotherSomething();
const s = new Something();
console.log(typeof Something); // "function"
console.log(typeof AnotherSomething); // "function"
console.log(as.toString()); // "[object Object]"
console.log(as.toString()); // "[object Object]"
console.log(as.toString === Object.prototype.toString); // true
console.log(s.toString === Object.prototype.toString); // true
45. 什么是模板字符串?
模板字符串是在 JS 中创建字符串的一种新方法。我们可以通过使用反引号使模板字符串化。
//ES5 Version
var greet = 'Hi I\'m Mark';
//ES6 Version
let greet = `Hi I'm Mark`;
在 ES5 中我们需要使用一些转义字符来达到多行的效果,在模板字符串不需要这么麻烦:
//ES5 Version
var lastWords = '\n'
+ ' I \n'
+ ' Am \n'
+ 'Iron Man \n';
//ES6 Version
let lastWords = `
I
Am
Iron Man
`;
在ES5版本中,我们需要添加\n
以在字符串中添加新行。在模板字符串中,我们不需要这样做。
//ES5 Version
function greet(name) {
return 'Hello ' + name + '!';
}
//ES6 Version
function greet(name) {
return `Hello ${name} !`;
}
在 ES5 版本中,如果需要在字符串中添加表达式或值,则需要使用+
运算符。在模板字符串s中,我们可以使用${expr}
嵌入一个表达式,这使其比 ES5 版本更整洁。
对象析构是从对象或数组中获取或提取值的一种新的、更简洁的方法。假设有如下的对象:
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;
使用解构方式语法就变得简洁多了:
{ firstName, lastName, position, yearHired } = employee;
我们还可以为属性取别名:
let { firstName: fName, lastName: lName, position, yearHired } = employee;
当然如果属性值为 undefined
时,我们还可以指定默认值:
let { firstName = "Mark", lastName: lName, position, yearHired } = employee;
47. 什么是 ES6 模块?
模块使我们能够将代码基础分割成多个文件,以获得更高的可维护性,并且避免将所有代码放在一个大文件中。在 ES6 支持模块之前,有两个流行的模块。
-
CommonJS-Node.js
-
AMD(异步模块定义)-浏览器
基本上,使用模块的方式很简单,import
用于从另一个文件中获取功能或几个功能或值,同时export
用于从文件中公开功能或几个功能或值。
导出
使用 ES5 (CommonJS)
// 使用 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 模块
// 使用 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);
}
在另一个文件中导入函数
// 使用 ES5 (CommonJS) - index.js
const helpers = require('./helpers.js'); // helpers is an object
const isNull = helpers.isNull;
const isUndefined = helpers.isUndefined;
const isNullOrUndefined = helpers.isNullOrUndefined;
// or if your environment supports Destructuring
const { isNull, isUndefined, isNullOrUndefined } = require('./helpers.js');
-------------------------------------------------------
// ES6 Modules - index.js
import * as helpers from './helpers.js'; // helpers is an object
// or
import { isNull, isUndefined, isNullOrUndefined as isValid } from './helpers.js';
// using "as" for renaming named exports
在文件中导出单个功能或默认导出
使用 ES5 (CommonJS)
// 使用 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
// 使用 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)
// 使用 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));
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
方法需要一个回调作为参数。回调的一个很好的类比是,当你打电话给某人,如果他们不接,你留下一条消息,你期待他们回调。调用某人或留下消息的行为是事件或数据,回调是你希望稍后发生的操作。
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) => {...});
由于篇幅过长,我将此系列分成上中下三篇,下篇我们在见。
原文:
https://dev.to/macmacky/70-javascript-interview-questions-5gfi#1-whats-the-difference-between-undefined-and-null