乐趣区

关于前端:2023年前端面试真题之JS篇

人的毕生,总是不免有浮沉。不会永远如旭日东升,也不会永远苦楚潦倒。重复地一浮一沉,对于一个人来说,正是磨难。因而,浮在下面的,不用自豪;沉在底下的,更用不着乐观。必须以率直、虚心的态度,乐观进取、向前迈进。——松下幸之助

大家好,我是江辰,在现在的互联网大环境下,想必大家都或多或少且有感触,塌实的社会之下,只有一直的放弃心性,能力感知不同的播种,互勉。

2023 年最新的面试题集锦,时刻做好筹备。

本文首发于微信公众号:家养程序猿江辰

欢送大家点赞,珍藏,关注

文章列表

  • 2023 年前端面试真题之编码篇
  • 2023 年前端面试真题之 CSS 篇
  • 2023 年前端面试真题之 HTML 篇
  • 2023 年前端面试真题之 React 篇
  • 2023 年前端面试真题之 Vue 篇

请简述 JavaScript 中的 this

JS 中的 this 是一个绝对简单的概念,不是简略几句能解释分明的。粗略地讲,函数的调用形式决定了 this 的值。我浏览了网上很多对于 this 的文章,Arnav Aggrawal 写的比较清楚。this 取值合乎以下规定:

在调用函数时应用 new 关键字,函数内的 this 是一个全新的对象。
如果 apply、callbind 办法用于调用、创立一个函数,函数内的 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、letconst 申明变量,就为一个变量赋值时,该变量是未声明变量(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); // undefined
console.log(foo === undefined); // true
console.log(typeof foo === 'undefined'); // true

console.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); // true

console.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)); // 3
console.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 通过 <script> 标签发送跨域申请,通常应用 callback 查问参数,例如:https://example.com?callback=printData。而后服务器将数据包装在一个名为 printData 的函数中并将其返回给客户端。

<!-- https://mydomain.com -->
<script>
  function printData(data) {console.log(`My name is ${data.name}!`);
  }
</script>

<script src="https://example.com?callback=printData"></script>
// 文件加载自 https://example.com?callback=printData
printData({name: 'Yang Shun'});

客户端必须在其全局范畴内具备 printData 函数,并且在收到来自跨域的响应时,该函数将由客户端执行。

JSONP 可能具备一些安全隐患。因为 JSONP 是纯 JavaScript 实现,它能够实现 JavaScript 所能做的所有,因而须要信赖 JSONP 数据的提供者。

现如今,跨起源资源共享(CORS)是举荐的支流形式,JSONP 已被视为一种比拟 hack 的形式。

请解释变量晋升(hoisting)。

变量晋升(hoisting)是用于解释代码中变量申明行为的术语。应用 var 关键字申明或初始化的变量,会将申明语句“晋升”到以后作用域的顶部。然而,只有申明才会触发晋升,赋值语句(如果有的话)将放弃原样。咱们用几个例子来解释一下。

// 用 var 申明失去晋升
console.log(foo); // undefined
var foo = 1;
console.log(foo); // 1

// 用 let/const 申明不会晋升
console.log(bar); // ReferenceError: bar is not defined
let 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); // undefined
bar(); // Uncaught TypeError: bar is not a function
var bar = function () {console.log('BARRRR');
};
console.log(bar); // [Function: bar]

请形容事件冒泡。

当一个事件在 DOM 元素上触发时,如果有事件监听器,它将尝试解决该事件,而后事件冒泡到其父级元素,并产生同样的事件。最初直到事件达到先人元素。事件冒泡是实现事件委托的原理(event delegation)。

== 和 === 的区别是什么

== 是形象相等运算符,而 === 是严格相等运算符。== 运算符是在进行必要的类型转换后,再比拟。=== 运算符不会进行类型转换,所以如果两个值不是雷同的类型,会间接返回 false。应用 == 时,可能产生一些特地的事件,例如:

1 == '1'; // true
1 == [1]; // true
1 == true; // true
0 == ''; // true
0 == '0'; // true
0 == false; // true

我的倡议是从不应用 == 运算符,除了不便与 nullundefined 比拟时,a == null 如果 anullundefined 将返回 true

var a = null;
console.log(a == null); // true
console.log(a == undefined); // true

请解释对于 JavaScript 的同源策略。

同源策略可避免 JavaScript 发动跨域申请。源被定义为 URI、主机名和端口号的组合。此策略可避免页面上的歹意脚本通过该页面的文档对象模型,拜访另一个网页上的敏感数据。

你对 Promises 及其 polyfill 的把握水平如何?

把握它的工作原理。Promise 是一个可能在将来某个工夫产生后果的对象:操作胜利的后果或失败的起因(例如产生网络谬误)。Promise 可能处于以下三种状态之一:fulfilledrejectedpending。用户能够对 Promise 增加回调函数来解决操作胜利的后果或失败的起因。

一些常见的 polyfill$.deferred、Q 和 Bluebird,但不是所有的 polyfill 都符合规范。ES2015 反对 Promises,当初通常不须要应用 polyfills。

Promise 代替回调函数有什么优缺点?

长处

  • 防止可读性极差的回调天堂。
  • 应用.then()编写的程序异步代码,既简略又易读。
  • 应用 Promise.all()编写并行异步代码变得很容易。

毛病

  • 轻微地减少了代码的复杂度(这点存在争议)。
  • 在不反对 ES2015 的旧版浏览器中,须要引入 polyfill 能力应用。

请解释同步和异步函数之间的区别。

同步函数阻塞,而异步函数不阻塞。在同步函数中,语句实现后,下一句才执行。在这种状况下,程序能够依照语句的程序进行准确评估,如果其中一个语句须要很长时间,程序的执行会停滞很长时间。

异步函数通常承受回调作为参数,在调用异步函数后立刻继续执行下一行。回调函数仅在异步操作实现且调用堆栈为空时调用。诸如从 Web 服务器加载数据或查询数据库等重负载操作应该异步实现,以便主线程能够继续执行其余操作,而不会呈现始终阻塞,直到费时操作实现的状况(在浏览器中,界面会卡住)。

什么是事件循环?调用堆栈和工作队列之间有什么区别?

事件循环是一个单线程循环,用于监督调用堆栈并查看是否有工作行将在工作队列中实现。如果调用堆栈为空并且工作队列中有回调函数,则将回调函数出队并推送到调用堆栈中执行。

应用 let、var 和 const 创立变量有什么区别?

var 申明的变量的作用域是它以后的执行上下文,它能够是嵌套的函数,也能够是申明在任何函数外的变量。letconst 是块级作用域,意味着它们只能在最近的一组花括号(function、if-else 代码块或 for 循环中)中拜访。

function foo() {
  // 所有变量在函数中都可拜访
  var bar = 'bar';
  let baz = 'baz';
  const qux = 'qux';

  console.log(bar); // bar
  console.log(baz); // baz
  console.log(qux); // qux
}

console.log(bar); // ReferenceError: bar is not defined
console.log(baz); // ReferenceError: baz is not defined
console.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 defined
console.log(qux); // ReferenceError: qux is not defined

var 会使变量晋升,这意味着变量能够在申明之前应用。letconst 不会使变量晋升,提前应用会报错。

console.log(foo); // undefined

var foo = 'foo';

console.log(baz); // ReferenceError: can't access lexical declaration'baz' before initialization

let baz = 'baz';

console.log(bar); // ReferenceError: can't access lexical declaration'bar' before initialization

const bar = 'bar';

var 反复申明不会报错,但 letconst 会。

var foo = 'foo';
var foo = 'bar';
console.log(foo); // "bar"

let baz = 'baz';
let baz = 'qux'; // Uncaught SyntaxError: Identifier 'baz' has already been declared

letconst 的区别在于:let 容许屡次赋值,而 const 只容许一次。

// 这样不会报错。let foo = 'foo';
foo = 'bar';

// 这样会报错。const baz = 'baz';
baz = 'qux';

你能给出一个应用箭头函数的例子吗,箭头函数与其余函数有什么不同

一个很显著的长处就是箭头函数能够简化创立函数的语法,咱们不须要在箭头函数后面加上 function 关键词。并且箭头函数的 this 会主动绑定到以后作用域的上下文中,这和一般的函数不一样。一般函数的 this 是在执行的时候能力确定的。箭头函数的这个特点对于回调函数来说特地有用,特地对于 React 组件而言。

高阶函数(higher-order)的定义是什么?

高阶函数是将一个或多个函数作为参数的函数,它用于数据处理,也可能将函数作为返回后果。高阶函数是为了形象一些反复执行的操作。一个典型的例子是 map,它将一个数组和一个函数作为参数。map 应用这个函数来转换数组中的每个元素,并返回一个蕴含转换后元素的新数组。JavaScript 中的其余常见示例是 forEachfilterreduce。高阶函数不仅须要操作数组的时候会用到,还有许多函数返回新函数的用例。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); // 3
console.log(b); // 1

对象解构

// 变量赋值
const o = {p: 42, q: true};
const {p, q} = o;

console.log(p); // 42
console.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]
退出移动版