写点什么

京东前端经典面试题整理

作者:loveX001
  • 2023-02-14
    浙江
  • 本文字数:8301 字

    阅读完需:约 27 分钟

img 的 srcset 属性的作⽤?

响应式页面中经常用到根据屏幕密度设置不同的图片。这时就用到了 img 标签的 srcset 属性。srcset 属性用于设置不同屏幕密度下,img 会自动加载不同的图片。用法如下:


<img src="image-128.png" srcset="image-256.png 2x" />
复制代码


使用上面的代码,就能实现在屏幕密度为 1x 的情况下加载 image-128.png, 屏幕密度为 2x 时加载 image-256.png。


按照上面的实现,不同的屏幕密度都要设置图片地址,目前的屏幕密度有 1x,2x,3x,4x 四种,如果每一个图片都设置 4 张图片,加载就会很慢。所以就有了新的 srcset 标准。代码如下:


<img src="image-128.png"     srcset="image-128.png 128w, image-256.png 256w, image-512.png 512w"     sizes="(max-width: 360px) 340px, 128px" />
复制代码


其中 srcset 指定图片的地址和对应的图片质量。sizes 用来设置图片的尺寸零界点。对于 srcset 中的 w 单位,可以理解成图片质量。如果可视区域小于这个质量的值,就可以使用。浏览器会自动选择一个最小的可用图片。


sizes 语法如下:


sizes="[media query] [length], [media query] [length] ... "
复制代码


sizes 就是指默认显示 128px, 如果视区宽度大于 360px, 则显示 340px。

左右居中方案

  • 行内元素: text-align: center

  • 定宽块状元素: 左右 margin 值为 auto

  • 不定宽块状元素: table布局,position + transform


/* 方案1 */.wrap {  text-align: center}.center {  display: inline;  /* or */  /* display: inline-block; */}/* 方案2 */.center {  width: 100px;  margin: 0 auto;}/* 方案2 */.wrap {  position: relative;}.center {  position: absulote;  left: 50%;  transform: translateX(-50%);}
复制代码

Loader 和 Plugin 有什么区别

Loader:直译为"加载器"。Webpack 将一切文件视为模块,但是 webpack 原生是只能解析 js 文件,如果想将其他文件也打包的话,就会用到loader。 所以 Loader 的作用是让 webpack 拥有了加载和解析非 JavaScript 文件的能力。 Plugin:直译为"插件"。Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

什么是 JavaScript 中的包装类型?

在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,如:


const a = "abc";a.length; // 3a.toUpperCase(); // "ABC"
复制代码


在访问'abc'.length时,JavaScript 将'abc'在后台转换成String('abc'),然后再访问其length属性。


JavaScript 也可以使用Object函数显式地将基本类型转换为包装类型:


var a = 'abc'Object(a) // String {"abc"}
复制代码


也可以使用valueOf方法将包装类型倒转成基本类型:


var a = 'abc'var b = Object(a)var c = b.valueOf() // 'abc'
复制代码


看看如下代码会打印出什么:


var a = new Boolean( false );if (!a) {    console.log( "Oops" ); // never runs}
复制代码


答案是什么都不会打印,因为虽然包裹的基本类型是false,但是false被包裹成包装类型后就成了对象,所以其非值为false,所以循环体中的内容不会运行。

为什么函数的 arguments 参数是类数组而不是数组?如何遍历类数组?

arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有calleelength等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。


要遍历类数组,有三个方法:


(1)将数组的方法应用到类数组上,这时候就可以使用callapply方法,如:


function foo(){   Array.prototype.forEach.call(arguments, a => console.log(a))}
复制代码


(2)使用 Array.from 方法将类数组转化成数组:‌


function foo(){   const arrArgs = Array.from(arguments)   arrArgs.forEach(a => console.log(a))}
复制代码


(3)使用展开运算符将类数组转化成数组


function foo(){     const arrArgs = [...arguments]     arrArgs.forEach(a => console.log(a)) }
复制代码

代码输出结果

function runAsync(x) {  const p = new Promise(r =>    setTimeout(() => r(x, console.log(x)), 1000)  );  return p;}function runReject(x) {  const p = new Promise((res, rej) =>    setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)  );  return p;}Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)])  .then(res => console.log("result: ", res))  .catch(err => console.log(err));
复制代码


输出结果如下:


0Error: 0123
复制代码


可以看到在 catch 捕获到第一个错误之后,后面的代码还不执行,不过不会再被捕获了。


注意:allrace传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被 then 的第二个参数或者后面的 catch 捕获;但并不会影响数组中其它的异步任务的执行。


参考 前端进阶面试题详细解答

事件是什么?事件模型?

事件是用户操作网页时发生的交互动作,比如 click/move, 事件除了用户触发的动作外,还可以是文档加载,窗口滚动和大小调整。事件被封装成一个 event 对象,包含了该事件发生时的所有相关信息( event 的属性)以及可以对事件进行的操作( event 的方法)。


事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型:


  • DOM0 级事件模型,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过 js 属性来指定监听函数。所有浏览器都兼容这种方式。直接在 dom 对象上注册事件名称,就是 DOM0 写法。

  • IE 事件模型,在该事件模型中,一次事件共有两个过程,事件处理阶段和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。

  • DOM2 级事件模型,在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。

== 操作符的强制类型转换规则?

对于 == 来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 xy 是否相同,就会进行如下判断流程:


  1. 首先会判断两者类型是否相同,相同的话就比较两者的大小;

  2. 类型不相同的话,就会进行类型转换;

  3. 会先判断是否在对比 nullundefined,是的话就会返回 true

  4. 判断两者类型是否为 stringnumber,是的话就会将字符串转换为 number


1 == '1'1 ==  1
复制代码


  1. 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断


'1' == true'1' ==  1 1  ==  1
复制代码


  1. 判断其中一方是否为 object 且另一方为 stringnumber 或者 symbol,是的话就会把 object 转为原始类型再进行判断


'1' == { name: 'js' }        ↓'1' == '[object Object]'
复制代码

原型修改、重写

function Person(name) {    this.name = name}// 修改原型Person.prototype.getName = function() {}var p = new Person('hello')console.log(p.__proto__ === Person.prototype) // trueconsole.log(p.__proto__ === p.constructor.prototype) // true// 重写原型Person.prototype = {    getName: function() {}}var p = new Person('hello')console.log(p.__proto__ === Person.prototype)        // trueconsole.log(p.__proto__ === p.constructor.prototype) // false
复制代码


可以看到修改原型的时候 p 的构造函数不是指向 Person 了,因为直接给 Person 的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数 Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用 constructor 指回来:


Person.prototype = {    getName: function() {}}var p = new Person('hello')p.constructor = Personconsole.log(p.__proto__ === Person.prototype)        // trueconsole.log(p.__proto__ === p.constructor.prototype) // true

复制代码

对 rest 参数的理解

扩展运算符被用在函数形参上时,它还可以把一个分离的参数序列整合成一个数组


function mutiple(...args) {  let result = 1;  for (var val of args) {    result *= val;  }  return result;}mutiple(1, 2, 3, 4) // 24
复制代码


这里,传入 mutiple 的是四个分离的参数,但是如果在 mutiple 函数里尝试输出 args 的值,会发现它是一个数组:


function mutiple(...args) {  console.log(args)}mutiple(1, 2, 3, 4) // [1, 2, 3, 4]
复制代码


这就是 … rest 运算符的又一层威力了,它可以把函数的多个入参收敛进一个数组里。这一点经常用于获取函数的多余参数,或者像上面这样处理函数参数个数不确定的情况。

JS 整数是怎么表示的?

  • 通过 Number 类型来表示,遵循 IEEE754 标准,通过 64 位来表示一个数字,(1 + 11 + 52),最大安全数字是 Math.pow(2, 53) - 1,对于 16 位十进制。(符号位 + 指数位 + 小数部分有效位)

代码输出结果

function runAsync (x) {  const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))  return p}function runReject (x) {  const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))  return p}Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])       .then(res => console.log(res))       .catch(err => console.log(err))
复制代码


输出结果如下:


// 1s后输出13// 2s后输出2Error: 2// 4s后输出4
复制代码


可以看到。catch 捕获到了第一个错误,在这道题目中最先的错误就是runReject(2)的结果。如果一组异步操作中有一个异常都不会进入.then()的第一个回调函数参数中。会被.then()的第二个回调函数捕获。

new 操作符的实现原理

new 操作符的执行过程:


(1)首先创建了一个新的空对象


(2)设置原型,将对象的原型设置为函数的 prototype 对象。


(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)


(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。


具体实现:


function objectFactory() {  let newObject = null;  let constructor = Array.prototype.shift.call(arguments);  let result = null;  // 判断参数是否是一个函数  if (typeof constructor !== "function") {    console.error("type error");    return;  }  // 新建一个空对象,对象的原型为构造函数的 prototype 对象  newObject = Object.create(constructor.prototype);  // 将 this 指向新建对象,并执行函数  result = constructor.apply(newObject, arguments);  // 判断返回对象  let flag = result && (typeof result === "object" || typeof result === "function");  // 判断返回结果  return flag ? result : newObject;}// 使用方法objectFactory(构造函数, 初始化参数);
复制代码

常见的 DOM 操作有哪些

1)DOM 节点的获取

DOM 节点的获取的 API 及使用:


getElementById // 按照 id 查询getElementsByTagName // 按照标签名查询getElementsByClassName // 按照类名查询querySelectorAll // 按照 css 选择器查询
// 按照 id 查询var imooc = document.getElementById('imooc') // 查询到 id 为 imooc 的元素// 按照标签名查询var pList = document.getElementsByTagName('p') // 查询到标签为 p 的集合console.log(divList.length)console.log(divList[0])// 按照类名查询var moocList = document.getElementsByClassName('mooc') // 查询到类名为 mooc 的集合// 按照 css 选择器查询var pList = document.querySelectorAll('.mooc') // 查询到类名为 mooc 的集合
复制代码

2)DOM 节点的创建

创建一个新节点,并把它添加到指定节点的后面。 已知的 HTML 结构如下:


<html>  <head>    <title>DEMO</title>  </head>  <body>    <div id="container">       <h1 id="title">我是标题</h1>    </div>     </body></html>
复制代码


要求添加一个有内容的 span 节点到 id 为 title 的节点后面,做法就是:


// 首先获取父节点var container = document.getElementById('container')// 创建新节点var targetSpan = document.createElement('span')// 设置 span 节点的内容targetSpan.innerHTML = 'hello world'// 把新创建的元素塞进父节点里去container.appendChild(targetSpan)
复制代码

3)DOM 节点的删除

删除指定的 DOM 节点, 已知的 HTML 结构如下:


<html>  <head>    <title>DEMO</title>  </head>  <body>    <div id="container">       <h1 id="title">我是标题</h1>    </div>     </body></html>
复制代码


需要删除 id 为 title 的元素,做法是:


// 获取目标元素的父元素var container = document.getElementById('container')// 获取目标元素var targetNode = document.getElementById('title')// 删除目标元素container.removeChild(targetNode)
复制代码


或者通过子节点数组来完成删除:


// 获取目标元素的父元素var container = document.getElementById('container')// 获取目标元素var targetNode = container.childNodes[1]// 删除目标元素container.removeChild(targetNode)
复制代码

4)修改 DOM 元素

修改 DOM 元素这个动作可以分很多维度,比如说移动 DOM 元素的位置,修改 DOM 元素的属性等。


将指定的两个 DOM 元素交换位置, 已知的 HTML 结构如下:


<html>  <head>    <title>DEMO</title>  </head>  <body>    <div id="container">       <h1 id="title">我是标题</h1>      <p id="content">我是内容</p>    </div>     </body></html>
复制代码


现在需要调换 title 和 content 的位置,可以考虑 insertBefore 或者 appendChild:


// 获取父元素var container = document.getElementById('container')   
// 获取两个需要被交换的元素var title = document.getElementById('title')var content = document.getElementById('content')// 交换两个元素,把 content 置于 title 前面container.insertBefore(content, title)
复制代码

Promise.resolve

Promise.resolve = function(value) {    // 1.如果 value 参数是一个 Promise 对象,则原封不动返回该对象    if(value instanceof Promise) return value;    // 2.如果 value 参数是一个具有 then 方法的对象,则将这个对象转为 Promise 对象,并立即执行它的then方法    if(typeof value === "object" && 'then' in value) {        return new Promise((resolve, reject) => {           value.then(resolve, reject);        });    }    // 3.否则返回一个新的 Promise 对象,状态为 fulfilled    return new Promise(resolve => resolve(value));}
复制代码

JavaScript 有哪些内置对象

全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。这里说的全局的对象是说在全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。


标准内置对象的分类:


(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。例如 Infinity、NaN、undefined、null 字面量


(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。例如 eval()、parseFloat()、parseInt() 等


(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。例如 Object、Function、Boolean、Symbol、Error 等


(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。例如 Number、Math、Date


(5)字符串,用来表示和操作字符串的对象。例如 String、RegExp


(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array


(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。例如 Map、Set、WeakMap、WeakSet


(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。例如 SIMD 等


(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。例如 JSON 等


(10)控制抽象对象例如 Promise、Generator 等


(11)反射。例如 Reflect、Proxy


(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。例如 Intl、Intl.Collator 等


(13)WebAssembly


(14)其他。例如 arguments


总结: js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。

JavaScript 类数组对象的定义?

一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。


常见的类数组转换为数组的方法有这样几种:


(1)通过 call 调用数组的 slice 方法来实现转换


Array.prototype.slice.call(arrayLike);
复制代码


(2)通过 call 调用数组的 splice 方法来实现转换


Array.prototype.splice.call(arrayLike, 0);
复制代码


(3)通过 apply 调用数组的 concat 方法来实现转换


Array.prototype.concat.apply([], arrayLike);
复制代码


(4)通过 Array.from 方法来实现转换


Array.from(arrayLike);
复制代码

对类数组对象的理解,如何转化为数组

一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,函数参数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。


常见的类数组转换为数组的方法有这样几种:


  • 通过 call 调用数组的 slice 方法来实现转换


Array.prototype.slice.call(arrayLike);
复制代码


  • 通过 call 调用数组的 splice 方法来实现转换


Array.prototype.splice.call(arrayLike, 0);
复制代码


  • 通过 apply 调用数组的 concat 方法来实现转换


Array.prototype.concat.apply([], arrayLike);
复制代码


  • 通过 Array.from 方法来实现转换


Array.from(arrayLike);
复制代码

说一下 JSON.stringify 有什么缺点?

1.如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式2.如果obj里有RegExp(正则表达式的缩写)、Error对象,则序列化的结果将只得到空对象;3、如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;4、如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null5、JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;6、如果对象中存在循环引用的情况也无法正确实现深拷贝;
复制代码

CSS 预处理器/后处理器是什么?为什么要使用它们?

预处理器, 如:lesssassstylus,用来预编译sass或者less,增加了css代码的复用性。层级,mixin, 变量,循环, 函数等对编写以及开发 UI 组件都极为方便。


后处理器, 如: postCss,通常是在完成的样式表中根据css规范处理css,让其更加有效。目前最常做的是给css属性添加浏览器私有前缀,实现跨浏览器兼容性的问题。


css预处理器为css增加一些编程特性,无需考虑浏览器的兼容问题,可以在CSS中使用变量,简单的逻辑程序,函数等在编程语言中的一些基本的性能,可以让css更加的简洁,增加适应性以及可读性,可维护性等。


其它css预处理器语言:Sass(Scss), Less, Stylus, Turbine, Swithch css, CSS Cacheer, DT Css


使用原因:


  • 结构清晰, 便于扩展

  • 可以很方便的屏蔽浏览器私有语法的差异

  • 可以轻松实现多重继承

  • 完美的兼容了CSS代码,可以应用到老项目中


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
京东前端经典面试题整理_JavaScript_loveX001_InfoQ写作社区