人的一生,总是难免有浮沉。不会永远如旭日东升,也不会永远痛苦潦倒。反复地一浮一沉,对于一个人来说,正是磨练。因此,浮在上面的,不必骄傲;沉在底下的,更用不着悲观。必须以率直、谦虚的态度,乐观进取、向前迈进。——松下幸之助
大家好,我是江辰,在如今的互联网大环境下,想必大家都或多或少且有感受,浮躁的社会之下,只有不断的保持心性,才能感知不同的收获,互勉。
2023年最新的面试题集锦,时刻做好准备。
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
表妹一键制作自己的五星红旗国庆头像,超好看
请简述JavaScript中的this
JS 中的this
是一个相对复杂的概念,不是简单几句能解释清楚的。粗略地讲,函数的调用方式决定了this
的值。我阅读了网上很多关于this
的文章,Arnav Aggrawal写的比较清楚。this
取值符合以下规则:
在调用函数时使用new
关键字,函数内的this
是一个全新的对象。 如果apply、call
或bind
方法用于调用、创建一个函数,函数内的this
就是作为参数传入这些方法的对象。 当函数作为对象里的方法被调用时,函数内的this
是调用该函数的对象。比如当obj.method()
被调用时,函数内的this
将绑定到obj
对象。 如果调用函数不符合上述规则,那么this
的值指向全局对象(global object
)。浏览器环境下this
的值指向window
对象,但是在严格模式下('use strict'
),this
的值为undefined
。 如果符合上述多个规则,则较高的规则(1 号最高,4 号最低)将决定this
的值。 如果该函数是ES2015
中的箭头函数,将忽略上面的所有规则,this
被设置为它被创建时的上下文。
说说你对 AMD 和 CommonJS 的了解。
它们都是实现模块体系的方式,直到ES2015
出现之前,JavaScript
一直没有模块体系。CommonJS
是同步的,而AMD(Asynchronous Module Definition)
从全称中可以明显看出是异步的。CommonJS
的设计是为服务器端开发考虑的,而AMD
支持异步加载模块,更适合浏览器。
我发现AMD
的语法非常冗长,CommonJS
更接近其他语言import
声明语句的用法习惯。大多数情况下,我认为AMD
没有使用的必要,因为如果把所有JavaScript
都捆绑进一个文件中,将无法得到异步加载的好处。此外,CommonJS
语法上更接近Node
编写模块的风格,在前后端都使用JavaScript
开发之间进行切换时,语境的切换开销较小。
我很高兴看到ES2015
的模块加载方案同时支持同步和异步,我们终于可以只使用一种方案了。虽然它尚未在浏览器和Node
中完全推出,但是我们可以使用代码转换工具进行转换。
请解释下面代码为什么不能用作 IIFE:function foo(){ }();,需要作出哪些修改才能使其成为 IIFE?
IIFE(Immediately Invoked Function Expressions)代表立即执行函数。JavaScript
解析器将function foo(){ }();
解析成function foo(){ }和();
。其中,前者是函数声明;后者(一对括号)是试图调用一个函数,却没有指定名称,因此它会抛出Uncaught SyntaxError: Unexpected token )
的错误。
修改方法是:再添加一对括号,形式上有两种:(function foo(){ })()
和(function foo(){ }())
。以上函数不会暴露到全局作用域,如果不需要在函数内部引用自身,可以省略函数的名称。
你可能会用到void
操作符:void function foo(){ }();
。但是,这种做法是有问题的。表达式的值是undefined
,所以如果你的IIFE
有返回值,不要用这种做法。例如
const foo = void (function bar() {return 'foo';})();console.log(foo); // undefined
null、undefined和未声明变量之间有什么区别?如何检查判断这些状态值?
当你没有提前使用var、let
或const
声明变量,就为一个变量赋值时,该变量是未声明变量(undeclared variables
)。未声明变量会脱离当前作用域,成为全局作用域下定义的变量。在严格模式下,给未声明的变量赋值,会抛出ReferenceError
错误。和使用全局变量一样,使用未声明变量也是非常不好的做法,应当尽可能避免。要检查判断它们,需要将用到它们的代码放在try/catch
语句中。
function foo() {x = 1; // 在严格模式下,抛出 ReferenceError 错误}foo();console.log(x); // 1
当一个变量已经声明,但没有赋值时,该变量的值是undefined
。如果一个函数的执行结果被赋值给一个变量,但是这个函数却没有返回任何值,那么该变量的值是undefined
。要检查它,需要使用严格相等(===
);或者使用typeof
,它会返回'undefined'
字符串。请注意,不能使用非严格相等(==
)来检查,因为如果变量值为null
,使用非严格相等也会返回true
。
var foo;console.log(foo); // undefinedconsole.log(foo === undefined); // trueconsole.log(typeof foo === 'undefined'); // trueconsole.log(foo == null); // true. 错误,不要使用非严格相等!function bar() {}var baz = bar();console.log(baz); // undefined
null
只能被显式赋值给变量。它表示空值,与被显式赋值undefined
的意义不同。要检查判断null
值,需要使用严格相等运算符。请注意,和前面一样,不能使用非严格相等(==
)来检查,因为如果变量值为undefined
,使用非严格相等也会返回true
。
var foo = null;console.log(foo === null); // trueconsole.log(foo == undefined); // true. 错误,不要使用非严格相等!
作为一种个人习惯,我从不使用未声明变量。如果定义了暂时没有用到的变量,我会在声明后明确地给它们赋值为null
什么是闭包(closure),为什么使用闭包?
闭包是函数和声明该函数的词法环境的组合。词法作用域中使用的域,是变量在代码中声明的位置所决定的。闭包是即使被外部函数返回,依然可以访问到外部(封闭)函数作用域的函数。
为什么使用闭包?
- 利用闭包实现数据私有化或模拟私有方法。这个方式也称为模块模式(
module pattern
)。 - 部分参数函数(
partial applications
)柯里化(currying
).
请说明.forEach循环和.map()循环的主要区别,它们分别在什么情况下使用?
为了理解两者的区别,我们看看它们分别是做什么的。
forEach
- 遍历数组中的元素。
- 为每个元素执行回调。
- 无返回值。
const a = [1, 2, 3];const doubled = a.forEach((num, index) => {// 执行与 num、index 相关的代码});// doubled = undefined
map
- 遍历数组中的元素
- 通过对每个元素调用函数,将每个元素“映射(map)”到一个新元素,从而创建一个新数组。
const a = [1, 2, 3];const doubled = a.map((num) => {return num * 2;});// doubled = [2, 4, 6]
.forEach
和.map()
的主要区别在于.map()
返回一个新的数组。如果你想得到一个结果,但不想改变原始数组,用.map()
。如果你只需要在数组上做迭代修改,用forEach
。
匿名函数的典型应用场景是什么?
匿名函数可以在 IIFE 中使用,来封装局部作用域内的代码,以便其声明的变量不会暴露到全局作用域。
(function () {// 一些代码。})();
匿名函数可以作为只用一次,不需要在其他地方使用的回调函数。当处理函数在调用它们的程序内部被定义时,代码具有更好地自闭性和可读性,可以省去寻找该处理函数的函数体位置的麻烦。
setTimeout(function () {console.log('Hello world!');}, 1000);
匿名函数可以用于函数式编程或 Lodash(类似于回调函数)。
const arr = [1, 2, 3];const double = arr.map(function (el) {return el * 2;});console.log(double); // [2, 4, 6]
.call和.apply有什么区别?
.call
和.apply
都用于调用函数,第一个参数将用作函数内this
的值。然而,.call
接受逗号分隔的参数作为后面的参数,而.apply
接受一个参数数组作为后面的参数。一个简单的记忆方法是,从call
中的 C 联想到逗号分隔(comma-separated
),从apply
中的 A 联想到数组(array
)。
function add(a, b) {return a + b;}console.log(add.call(null, 1, 2)); // 3console.log(add.apply(null, [1, 2])); // 3
请说明Function.prototype.bind的用法。
摘自MDN:
bind()方法创建一个新的函数, 当被调用时,将其 this 关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。
根据我的经验,将this
的值绑定到想要传递给其他函数的类的方法中是非常有用的。在React
组件中经常这样做。
请尽可能详细地解释 Ajax。
Ajax(asynchronous JavaScript and XML)是使用客户端上的许多 Web 技术,创建异步 Web 应用的一种 Web 开发技术。借助 Ajax,Web 应用可以异步(在后台)向服务器发送数据和从服务器检索数据,而不会干扰现有页面的显示和行为。通过将数据交换层与表示层分离,Ajax 允许网页和扩展 Web 应用程序动态更改内容,而无需重新加载整个页面。实际上,现在通常将 XML 替换为 JSON,因为 JavaScript 对 JSON 有原生支持优势。
XMLHttpRequest API
经常用于异步通信。此外还有最近流行的fetch API
。
使用 Ajax 的优缺点分别是什么?
优点
- 交互性更好。来自服务器的新内容可以动态更改,无需重新加载整个页面。
- 减少与服务器的连接,因为脚本和样式只需要被请求一次。
- 状态可以维护在一个页面上。JavaScript 变量和 DOM 状态将得到保持,因为主容器页面未被重新加载。
- 基本上包括大部分 SPA 的优点。
缺点
- 动态网页很难收藏。
- 如果 JavaScript 已在浏览器中被禁用,则不起作用。
- 有些网络爬虫不执行 JavaScript,也不会看到 JavaScript 加载的内容。
- 基本上包括大部分 SPA 的缺点
请说明 JSONP 的工作原理,它为什么不是真正的 Ajax?
JSONP(带填充的 JSON)是一种通常用于绕过 Web 浏览器中的跨域限制的方法,因为 Ajax 不允许跨域请求。
JSONP 通过标签发送跨域请求,通常使用
callback
查询参数,例如:https://example.com" />function printData(data) {console.log(`My name is ${data.name}!`);}
// 文件加载自 https://example.com?callback=printDataprintData({name: 'Yang Shun'});
客户端必须在其全局范围内具有printData
函数,并且在收到来自跨域的响应时,该函数将由客户端执行。
JSONP 可能具有一些安全隐患。由于 JSONP 是纯 JavaScript 实现,它可以完成 JavaScript 所能做的一切,因此需要信任 JSONP 数据的提供者。
现如今,跨来源资源共享(CORS) 是推荐的主流方式,JSONP 已被视为一种比较 hack 的方式。
请解释变量提升(hoisting)。
变量提升(hoisting)是用于解释代码中变量声明行为的术语。使用var
关键字声明或初始化的变量,会将声明语句“提升”到当前作用域的顶部。 但是,只有声明才会触发提升,赋值语句(如果有的话)将保持原样。我们用几个例子来解释一下。
// 用 var 声明得到提升console.log(foo); // undefinedvar foo = 1;console.log(foo); // 1// 用 let/const 声明不会提升console.log(bar); // ReferenceError: bar is not definedlet bar = 2;console.log(bar); // 2
函数声明会使函数体提升,但函数表达式(以声明变量的形式书写)只有变量声明会被提升。
// 函数声明console.log(foo); // [Function: foo]foo(); // 'FOOOOO'function foo() {console.log('FOOOOO');}console.log(foo); // [Function: foo]// 函数表达式console.log(bar); // undefinedbar(); // Uncaught TypeError: bar is not a functionvar bar = function () {console.log('BARRRR');};console.log(bar); // [Function: bar]
请描述事件冒泡。
当一个事件在 DOM 元素上触发时,如果有事件监听器,它将尝试处理该事件,然后事件冒泡到其父级元素,并发生同样的事情。最后直到事件到达祖先元素。事件冒泡是实现事件委托的原理(event delegation)。
==和===的区别是什么
==
是抽象相等运算符,而===
是严格相等运算符。==
运算符是在进行必要的类型转换后,再比较。===
运算符不会进行类型转换,所以如果两个值不是相同的类型,会直接返回false
。使用==
时,可能发生一些特别的事情,例如:
1 == '1'; // true1 == [1]; // true1 == true; // true0 == ''; // true0 == '0'; // true0 == false; // true
我的建议是从不使用==
运算符,除了方便与null
或undefined
比较时,a == null
如果a
为null
或undefined
将返回true
var a = null;console.log(a == null); // trueconsole.log(a == undefined); // true
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
请解释关于 JavaScript 的同源策略。
同源策略可防止 JavaScript 发起跨域请求。源被定义为 URI、主机名和端口号的组合。此策略可防止页面上的恶意脚本通过该页面的文档对象模型,访问另一个网页上的敏感数据。
你对 Promises 及其 polyfill 的掌握程度如何?
掌握它的工作原理。Promise
是一个可能在未来某个时间产生结果的对象:操作成功的结果或失败的原因(例如发生网络错误)。Promise
可能处于以下三种状态之一:fulfilled
、rejected
或pending
。 用户可以对Promise
添加回调函数来处理操作成功的结果或失败的原因。
一些常见的polyfill
是$.deferred
、Q 和 Bluebird,但不是所有的 polyfill 都符合规范。ES2015 支持 Promises,现在通常不需要使用 polyfills。
Promise代替回调函数有什么优缺点?
优点
- 避免可读性极差的回调地狱。
- 使用.then()编写的顺序异步代码,既简单又易读。
- 使用Promise.all()编写并行异步代码变得很容易。
缺点
- 轻微地增加了代码的复杂度(这点存在争议)。
- 在不支持 ES2015 的旧版浏览器中,需要引入 polyfill 才能使用。
请解释同步和异步函数之间的区别。
同步函数阻塞,而异步函数不阻塞。在同步函数中,语句完成后,下一句才执行。在这种情况下,程序可以按照语句的顺序进行精确评估,如果其中一个语句需要很长时间,程序的执行会停滞很长时间。
异步函数通常接受回调作为参数,在调用异步函数后立即继续执行下一行。回调函数仅在异步操作完成且调用堆栈为空时调用。诸如从 Web 服务器加载数据或查询数据库等重负载操作应该异步完成,以便主线程可以继续执行其他操作,而不会出现一直阻塞,直到费时操作完成的情况(在浏览器中,界面会卡住)。
什么是事件循环?调用堆栈和任务队列之间有什么区别?
事件循环是一个单线程循环,用于监视调用堆栈并检查是否有工作即将在任务队列中完成。如果调用堆栈为空并且任务队列中有回调函数,则将回调函数出队并推送到调用堆栈中执行。
使用let、var和const创建变量有什么区别?
用var
声明的变量的作用域是它当前的执行上下文,它可以是嵌套的函数,也可以是声明在任何函数外的变量。let
和const
是块级作用域,意味着它们只能在最近的一组花括号(function、if-else 代码块或 for 循环中)中访问。
function foo() {// 所有变量在函数中都可访问var bar = 'bar';let baz = 'baz';const qux = 'qux';console.log(bar); // barconsole.log(baz); // bazconsole.log(qux); // qux}console.log(bar); // ReferenceError: bar is not definedconsole.log(baz); // ReferenceError: baz is not definedconsole.log(qux); // ReferenceError: qux is not defined
if (true) {var bar = 'bar';let baz = 'baz';const qux = 'qux';}// 用 var 声明的变量在函数作用域上都可访问console.log(bar); // bar// let 和 const 定义的变量在它们被定义的语句块之外不可访问console.log(baz); // ReferenceError: baz is not definedconsole.log(qux); // ReferenceError: qux is not defined
var
会使变量提升,这意味着变量可以在声明之前使用。let
和const
不会使变量提升,提前使用会报错。
console.log(foo); // undefinedvar foo = 'foo';console.log(baz); // ReferenceError: can't access lexical declaration 'baz' before initializationlet baz = 'baz';console.log(bar); // ReferenceError: can't access lexical declaration 'bar' before initializationconst bar = 'bar';
用var
重复声明不会报错,但let
和const
会。
var foo = 'foo';var foo = 'bar';console.log(foo); // "bar"let baz = 'baz';let baz = 'qux'; // Uncaught SyntaxError: Identifier 'baz' has already been declared
let
和const
的区别在于:let
允许多次赋值,而const
只允许一次。
// 这样不会报错。let foo = 'foo';foo = 'bar';// 这样会报错。const baz = 'baz';baz = 'qux';
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
你能给出一个使用箭头函数的例子吗,箭头函数与其他函数有什么不同
一个很明显的优点就是箭头函数可以简化创建函数的语法,我们不需要在箭头函数前面加上function
关键词。并且箭头函数的this
会自动绑定到当前作用域的上下文中,这和普通的函数不一样。普通函数的this
是在执行的时候才能确定的。箭头函数的这个特点对于回调函数来说特别有用,特别对于React
组件而言。
高阶函数(higher-order)的定义是什么?
高阶函数是将一个或多个函数作为参数的函数,它用于数据处理,也可能将函数作为返回结果。高阶函数是为了抽象一些重复执行的操作。一个典型的例子是map
,它将一个数组和一个函数作为参数。map
使用这个函数来转换数组中的每个元素,并返回一个包含转换后元素的新数组。JavaScript 中的其他常见示例是forEach
、filter
和reduce
。高阶函数不仅需要操作数组的时候会用到,还有许多函数返回新函数的用例。Function.prototype.bind
就是一个例子。
Map 示例
假设我们有一个由名字组成的数组,我们需要将每个字符转换为大写字母。
const names = ['irish', 'daisy', 'anna'];
不使用高阶函数的方法是这样:
const transformNamesToUppercase = function (names) {const results = [];for (let i = 0; i < names.length; i++) {results.push(names[i].toUpperCase());}return results;};transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']
使用.map(transformerFn)
使代码更简明
const transformNamesToUppercase = function (names) {return names.map((name) => name.toUpperCase());};transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']
请给出一个解构(destructuring)对象或数组的例子。
解构是 ES6 中新功能,它提供了一种简洁方便的方法来提取对象或数组的值,并将它们放入不同的变量中。
数组解构
// 变量赋值const foo = ['one', 'two', 'three'];const [one, two, three] = foo;console.log(one); // "one"console.log(two); // "two"console.log(three); // "three"
// 变量交换let a = 1;let b = 3;[a, b] = [b, a];console.log(a); // 3console.log(b); // 1
对象解构
// 变量赋值const o = {p: 42, q: true};const {p, q} = o;console.log(p); // 42console.log(q); // true
你能举出一个柯里化函数(curry function)的例子吗?它有哪些好处?
柯里化(currying)是一种模式,其中具有多个参数的函数被分解为多个函数,当被串联调用时,将一次一个地累积所有需要的参数。这种技术帮助编写函数式风格的代码,使代码更易读、紧凑。值得注意的是,对于需要被 curry 的函数,它需要从一个函数开始,然后分解成一系列函数,每个函数都需要一个参数。
function curry(fn) {if (fn.length === 0) {return fn;}function _curried(depth, args) {return function (newArgument) {if (depth - 1 === 0) {return fn(...args, newArgument);}return _curried(depth - 1, [...args, newArgument]);};}return _curried(fn.length, []);}function add(a, b) {return a + b;}var curriedAdd = curry(add);var addFive = curriedAdd(5);var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
表妹一键制作自己的五星红旗国庆头像,超好看