写点什么

京东前端常考面试题(附答案)

作者:coder2028
  • 2022 年 9 月 13 日
    浙江
  • 本文字数:14199 字

    阅读完需:约 47 分钟

实现一个三角形

CSS 绘制三角形主要用到的是 border 属性,也就是边框。


平时在给盒子设置边框时,往往都设置很窄,就可能误以为边框是由矩形组成的。实际上,border 属性是右三角形组成的,下面看一个例子:


div {    width: 0;    height: 0;    border: 100px solid;    border-color: orange blue red green;}
复制代码


将元素的长宽都设置为 0


(1)三角 1


div {    width: 0;    height: 0;    border-top: 50px solid red;    border-right: 50px solid transparent;    border-left: 50px solid transparent;}
复制代码


(2)三角 2


div {    width: 0;    height: 0;    border-bottom: 50px solid red;    border-right: 50px solid transparent;    border-left: 50px solid transparent;}
复制代码


(3)三角 3


div {    width: 0;    height: 0;    border-left: 50px solid red;    border-top: 50px solid transparent;    border-bottom: 50px solid transparent;}
复制代码


(4)三角 4


div {    width: 0;    height: 0;    border-right: 50px solid red;    border-top: 50px solid transparent;    border-bottom: 50px solid transparent;}
复制代码


(5)三角 5


div {    width: 0;    height: 0;    border-top: 100px solid red;    border-right: 100px solid transparent;}
复制代码


还有很多,就不一一实现了,总体的原则就是通过上下左右边框来控制三角形的方向,用边框的宽度比来控制三角形的角度。

display 的 block、inline 和 inline-block 的区别

(1)block: 会独占一行,多个元素会另起一行,可以设置 width、height、margin 和 padding 属性;


(2)inline: 元素不会独占一行,设置 width、height 属性无效。但可以设置水平方向的 margin 和 padding 属性,不能设置垂直方向的 padding 和 margin;


(3)inline-block: 将对象设置为 inline 对象,但对象的内容作为 block 对象呈现,之后的内联对象会被排列在同一行内。


对于行内元素和块级元素,其特点如下:


(1)行内元素


  • 设置宽高无效;

  • 可以设置水平方向的 margin 和 padding 属性,不能设置垂直方向的 padding 和 margin;

  • 不会自动换行;


(2)块级元素


  • 可以设置宽高;

  • 设置 margin 和 padding 都有效;

  • 可以自动换行;

  • 多个块状,默认排列从上到下。

JS 闭包,你了解多少?

应该有面试官问过你:


  1. 什么是闭包?

  2. 闭包有哪些实际运用场景?

  3. 闭包是如何产生的?

  4. 闭包产生的变量如何被回收?


这些问题其实都可以被看作是同一个问题,那就是面试官在问你:你对JS闭包了解多少?


来总结一下我听到过的答案,尽量完全复原候选人面试的时候说的原话。


答案1: 就是一个function里面return了一个子函数,子函数访问了外面那个函数的变量。


答案2: for 循环里面可以用闭包来解决问题。


for(var i = 0; i < 10; i++){    setTimeout(()=>console.log(i),0)}// 控制台输出10遍10.for(var i = 0; i < 10; i++){    (function(a){    setTimeout(()=>console.log(a),0)    })(i)} // 控制台输出0-9
复制代码


答案3: 当前作用域产产生了对父作用域的引用。


答案4: 不知道。是跟浏览器的垃圾回收机制有关吗?


开杠了。请问,小伙伴的答案和以上的内容有多少相似程度?


其实,拿着这些问题好好想想,你就会发现这些问题都只是为了最终那一个问题。


闭包的底层实现原理


1. JS执行上下文


我们都知道,我们手写的 js 代码是要经过浏览器 V8 进行预编译后才能真正的被执行。例如变量提升、函数提升。举个栗子。


// 栗子:var d = 'abc';function a(){    console.log("函数a");};console.log(a);   // ƒ a(){ console.log("函数a"); }a();              // '函数a'var a = "变量a";  console.log(a);   // '变量a'a();              // a is not a functionvar c = 123;
// 输出结果及顺序:// ƒ a(){ console.log("函数a"); }// '函数a'// '变量a'// a is not a function
// 栗子预编后相当于:function a(){ console.log("函数a");};var d;console.log(a); // ƒ a(){ console.log("函数a"); }a(); // '函数a'
a = "变量a"; // 此时变量a赋值,函数声明被覆盖
console.log(a); // "变量a"a(); // a is not a function
复制代码


那么问题来了。 请问是谁来执行预编译操作的?那这个谁又是在哪里进行预编译的?


是的,你的疑惑没有错。js 代码运行需要一个运行环境,那这个环境就是执行上下文。 是的,js 运行前的预编译也是在这个环境中进行。


js 执行上下文分三种:


  • 全局执行上下文: 代码开始执行时首先进入的环境。

  • 函数执行上下文:函数调用时,会开始执行函数中的代码。

  • eval执行上下文:不建议使用,可忽略。


那么,执行上下文的周期,分为两个阶段:


  • 创建阶段

  • 创建词法环境

  • 生成变量对象(VO),建立作用域链作用域链作用域链(重要的事说三遍)

  • 确认this指向,并绑定this

  • 执行阶段。这个阶段进行变量赋值,函数引用及执行代码。


你现在猜猜看,预编译是发生在什么时候?


噢,我忘记说了,其实与编译还有另一个称呼:执行期上下文


预编译发生在函数执行之前。预编译四部曲为:


  1. 创建AO对象

  2. 找形参和变量声明,将变量和形参作为 AO 属性名,值为undefined

  3. 将实参和形参相统一

  4. 在函数体里找到函数声明,值赋予函数体。最后程序输出变量值的时候,就是从AO对象中拿。


所以,预编译真正的结果是:


var AO = {    a = function a(){console.log("函数a");};    d = 'abc'}
复制代码


我们重新来。

1. 什么叫变量对象?

变量对象是 js 代码在进入执行上下文时,js 引擎在内存中建立的一个对象,用来存放当前执行环境中的变量。

2. 变量对象(VO)的创建过程

变量对象的创建,是在执行上下文创建阶段,依次经过以下三个过程:


  • 创建 arguments 对象。

  • 对于函数执行环境,首先查询是否有传入的实参,如果有,则会将参数名是实参值组成的键值对放入arguments 对象中。否则,将参数名和 undefined组成的键值对放入 arguments 对象中。


//举个栗子 function bar(a, b, c) {    console.log(arguments);  // [1, 2]    console.log(arguments[2]); // undefined}bar(1,2)
复制代码


  • 当遇到同名的函数时,后面的会覆盖前面的。


console.log(a); // function a() {console.log('Is a ?') }function a() {    console.log('Is a');}function a() {  console.log('Is a ?')}
/**ps: 在执行第一行代码之前,函数声明已经创建完成.后面的对之前的声明进行了覆盖。**/
复制代码


  • 检查当前环境中的变量声明并赋值为undefined。当遇到同名的函数声明,为了避免函数被赋值为 undefined ,会忽略此声明


console.log(a); // function a() {console.log('Is a ?') }console.log(b); // undefinedfunction a() {  console.log('Is a ');}function a() {console.log('Is a ?');}var b = 'Is b';var a = 10086;
/**这段代码执行一下,你会发现 a 打印结果仍旧是一个函数,而 b 则是 undefined。**/
复制代码


根据以上三个步骤,对于变量提升也就知道是怎么回事了。

3. 变量对象变为活动对象

执行上下文的第二个阶段,称为执行阶段,在此时,会进行变量赋值,函数引用并执行其他代码,此时,变量对象变为活动对象。


我们还是举上面的例子:


console.log(a); // function a() {console.log('fjdsfs') }console.log(b); // undefinedfunction a() {   console.log('Is a');}function a() { console.log('Is a?');}var b = 'Is b';console.log(b); // 'Is b'var a = 10086; console.log(a);  // 10086var b = 'Is b?';console.log(b); // 'Is b?'
复制代码


在上面的代码中,代码真正开始执行是从第一行 console.log() 开始的,自这之前,执行上下文是这样的:


// 创建过程EC= {  VO: {}; // 创建变量对象  scopeChain: {}; // 作用域链}VO = {  argument: {...}; // 当前为全局上下文,所以这个属性值是空的  a: <a reference> // 函数 a  的引用地址  b: undefiend  // 见上文创建变量对象的第三步}
复制代码
词法作用域(Lexical scope

这里想说明,我们在函数执行上下文中有变量,在全局执行上下文中有变量。JavaScript的一个复杂之处在于它如何查找变量,如果在函数执行上下文中找不到变量,它将在调用上下文中寻找它,如果在它的调用上下文中没有找到,就一直往上一级,直到它在全局执行上下文中查找为止。(如果最后找不到,它就是 undefined)。


再来举个栗子:


 1: let top = 0; //  2: function createWarp() { 3:   function add(a, b) { 4:     let ret = a + b 5:     return ret 6:   } 7:   return add 8: } 9: let sum = createWarp()10: let result = sum(top, 8)11: console.log('result:',result)
复制代码


分析过程如下:


  • 在全局上下文中声明变量top 并赋值为 0.

  • 2 - 8 行。在全局执行上下文中声明了一个名为 createWarp 的变量,并为其分配了一个函数定义。其中第 3-7 行描述了其函数定义,并将函数定义存储到那个变量(createWarp)中。

  • 第 9 行。我们在全局执行上下文中声明了一个名为 sum 的新变量,暂时,值为 undefined

  • 第 9 行。遇到(),表明需要执行或调用一个函数。那么查找全局执行上下文的内存并查找名为 createWarp 的变量。 明显,已经在步骤 2 中创建完毕。接着,调用它。

  • 调用函数时,回到第 2 行。创建一个新的createWarp执行上下文。我们可以在 createWarp 的执行上下文中创建自有变量。js 引擎createWarp 的上下文添加到调用堆栈(call stack)。因为这个函数没有参数,直接跳到它的主体部分.

  • 3 - 6 行。我们有一个新的函数声明,createWarp执行上下文中创建一个变量 addadd 只存在于 createWarp 执行上下文中, 其函数定义存储在名为 add 的自有变量中。

  • 第 7 行,我们返回变量 add 的内容。js 引擎查找一个名为 add 的变量并找到它. 第 4 行和第 5 行括号之间的内容构成该函数定义。

  • createWarp 调用完毕,createWarp 执行上下文将被销毁。add 变量也跟着被销毁。add 函数定义仍然存在,因为它返回并赋值给了 sum 变量。 (ps: 这才是闭包产生的变量存于内存当中的真相

  • 接下来就是简单的执行过程,不再赘述。。

  • ……

  • 代码执行完毕,全局执行上下文被销毁。sum 和 result 也跟着被销毁。


小结一下


现在,如果再让你回答什么是闭包,你能答出多少?


其实,大家说的都对。不管是函数返回一个函数,还是产生了外部作用域的引用,都是有道理的。


所以,什么是闭包?


  • 解释一下作用域链是如何产生的。

  • 解释一下 js 执行上下文的创建、执行过程。

  • 解释一下闭包所产生的变量放在哪了。

  • 最后请把以上 3 点结合起来说给面试官听。

代码输出结果

function Dog() {  this.name = 'puppy'}Dog.prototype.bark = () => {  console.log('woof!woof!')}const dog = new Dog()console.log(Dog.prototype.constructor === Dog && dog.constructor === Dog && dog instanceof Dog)
复制代码


输出结果:true


解析: 因为 constructor 是 prototype 上的属性,所以 dog.constructor 实际上就是指向 Dog.prototype.constructor;constructor 属性指向构造函数。instanceof 而实际检测的是类型是否在实例的原型链上。


constructor 是 prototype 上的属性,这一点很容易被忽略掉。constructor 和 instanceof 的作用是不同的,感性地来说,constructor 的限制比较严格,它只能严格对比对象的构造函数是不是指定的值;而 instanceof 比较松散,只要检测的类型在原型链上,就会返回 true。

什么是同源策略

跨域问题其实就是浏览器的同源策略造成的。


同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的一个用于隔离潜在恶意文件的重要的安全机制。同源指的是:协议端口号域名必须一致。


同源策略:protocol(协议)、domain(域名)、port(端口)三者必须一致。


同源政策主要限制了三个方面:


  • 当前域下的 js 脚本不能够访问其他域下的 cookie、localStorage 和 indexDB。

  • 当前域下的 js 脚本不能够操作访问操作其他域下的 DOM。

  • 当前域下 ajax 无法发送跨域请求。


同源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者 script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。

图片懒加载

实现getBoundClientRect 的实现方式,监听 scroll 事件(建议给监听事件添加节流),图片加载完会从 img 标签组成的 DOM 列表中删除,最后所有的图片加载完毕后需要解绑监听事件。


// scr 加载默认图片,data-src 保存实施懒加载后的图片// <img src="./default.jpg" data-src="https://xxx.jpg" alt="" />let imgs = [...document.querySelectorAll("img")];const len = imgs.length;
let lazyLoad = function() { let count = 0; let deleteImgs = []; // 获取当前可视区的高度 let viewHeight = document.documentElement.clientHeight; // 获取当前滚动条的位置(距离顶部的距离,等价于document.documentElement.scrollTop) let scrollTop = window.pageYOffset; imgs.forEach((img) => { // 获取元素的大小,及其相对于视口的位置,如 bottom 为元素底部到网页顶部的距离 let bound = img.getBoundingClientRect(); // 当前图片距离网页顶部的距离 // let imgOffsetTop = img.offsetTop;
// 判断图片是否在可视区内,如果在就加载(两种判断方式) // if(imgOffsetTop < scrollTop + viewHeight) if (bound.top < viewHeight) { img.src = img.dataset.src; // 替换待加载的图片 src count++; deleteImgs.push(img); // 最后所有的图片加载完毕后需要解绑监听事件 if(count === len) { document.removeEventListener("scroll", imgThrottle); } } }); // 图片加载完会从 `img` 标签组成的 DOM 列表中删除 imgs = imgs.filter((img) => !deleteImgs.includes(img));}
window.onload = function () { lazyLoad();};// 使用 防抖/节流 优化一下滚动事件let imgThrottle = debounce(lazyLoad, 1000);// 监听 `scroll` 事件window.addEventListener("scroll", imgThrottle);
复制代码

settimeout 模拟实现 setinterval(带清除定时器的版本)

题目描述:setinterval 用来实现循环定时调用 可能会存在一定的问题 能用 settimeout 解决吗


实现代码如下:


function mySettimeout(fn, t) {  let timer = null;  function interval() {    fn();    timer = setTimeout(interval, t);  }  interval();  return {    cancel:()=>{      clearTimeout(timer)    }  }}// let a=mySettimeout(()=>{//   console.log(111);// },1000)// let b=mySettimeout(() => {//   console.log(222)// }, 1000)
复制代码


扩展:我们能反过来使用 setinterval 模拟实现 settimeout 吗?


const mySetTimeout = (fn, time) => {  const timer = setInterval(() => {    clearInterval(timer);    fn();  }, time);};// mySetTimeout(()=>{//   console.log(1);// },1000)
复制代码


扩展思考:为什么要用 settimeout 模拟实现 setinterval?setinterval 的缺陷是什么?


答案请自行百度哈 这个其实面试官问的也挺多的 小编这里就不展开了

文档声明(Doctype)和<!Doctype html>有何作用? 严格模式与混杂模式如何区分?它们有何意义?

文档声明的作用: 文档声明是为了告诉浏览器,当前HTML文档使用什么版本的HTML来写的,这样浏览器才能按照声明的版本来正确的解析。


的作用:<!doctype html> 的作用就是让浏览器进入标准模式,使用最新的 HTML5 标准来解析渲染页面;如果不写,浏览器就会进入混杂模式,我们需要避免此类情况发生。


严格模式与混杂模式的区分:


  • 严格模式: 又称为标准模式,指浏览器按照W3C标准解析代码;

  • 混杂模式: 又称怪异模式、兼容模式,是指浏览器用自己的方式解析代码。混杂模式通常模拟老式浏览器的行为,以防止老站点无法工作;


区分:网页中的DTD,直接影响到使用的是严格模式还是浏览模式,可以说DTD的使用与这两种方式的区别息息相关。


  • 如果文档包含严格的DOCTYPE ,那么它一般以严格模式呈现(严格 DTD ——严格模式);

  • 包含过渡 DTDURIDOCTYPE ,也以严格模式呈现,但有过渡 DTD 而没有 URI (统一资源标识符,就是声明最后的地址)会导致页面以混杂模式呈现(有 URI 的过渡 DTD ——严格模式;没有 URI 的过渡 DTD ——混杂模式);

  • DOCTYPE 不存在或形式不正确会导致文档以混杂模式呈现(DTD 不存在或者格式不正确——混杂模式);

  • HTML5 没有 DTD ,因此也就没有严格模式与混杂模式的区别,HTML5 有相对宽松的 法,实现时,已经尽可能大的实现了向后兼容(HTML5 没有严格和混杂之分)。


总之,严格模式让各个浏览器统一执行一套规范兼容模式保证了旧网站的正常运行。

ES6 中模板语法与字符串处理

ES6 提出了“模板语法”的概念。在 ES6 以前,拼接字符串是很麻烦的事情:


var name = 'css'   var career = 'coder' var hobby = ['coding', 'writing']var finalString = 'my name is ' + name + ', I work as a ' + career + ', I love ' + hobby[0] + ' and ' + hobby[1]
复制代码


仅仅几个变量,写了这么多加号,还要时刻小心里面的空格和标点符号有没有跟错地方。但是有了模板字符串,拼接难度直线下降:


var name = 'css'   var career = 'coder' var hobby = ['coding', 'writing']var finalString = `my name is ${name}, I work as a ${career} I love ${hobby[0]} and ${hobby[1]}`
复制代码


字符串不仅更容易拼了,也更易读了,代码整体的质量都变高了。这就是模板字符串的第一个优势——允许用 ${}的方式嵌入变量。但这还不是问题的关键,模板字符串的关键优势有两个:


  • 在模板字符串中,空格、缩进、换行都会被保留

  • 模板字符串完全支持“运算”式的表达式,可以在 ${}里完成一些计算


基于第一点,可以在模板字符串里无障碍地直接写 html 代码:


let list = `    <ul>        <li>列表项1</li>        <li>列表项2</li>    </ul>`;console.log(message); // 正确输出,不存在报错
复制代码


基于第二点,可以把一些简单的计算和调用丢进 ${} 来做:


function add(a, b) {  const finalString = `${a} + ${b} = ${a+b}`  console.log(finalString)}add(1, 2) // 输出 '1 + 2 = 3'
复制代码


除了模板语法外, ES6 中还新增了一系列的字符串方法用于提升开发效率:


(1)存在性判定:在过去,当判断一个字符/字符串是否在某字符串中时,只能用 indexOf > -1 来做。现在 ES6 提供了三个方法:includes、startsWith、endsWith,它们都会返回一个布尔值来告诉你是否存在。


  • includes:判断字符串与子串的包含关系:


const son = 'haha' const father = 'xixi haha hehe'father.includes(son) // true
复制代码


  • startsWith:判断字符串是否以某个/某串字符开头:


const father = 'xixi haha hehe'father.startsWith('haha') // falsefather.startsWith('xixi') // true
复制代码


  • endsWith:判断字符串是否以某个/某串字符结尾:


const father = 'xixi haha hehe'  father.endsWith('hehe') // true
复制代码


(2)自动重复:可以使用 repeat 方法来使同一个字符串输出多次(被连续复制多次):


const sourceCode = 'repeat for 3 times;'const repeated = sourceCode.repeat(3) console.log(repeated) // repeat for 3 times;repeat for 3 times;repeat for 3 times;
复制代码

说一下原型链和原型链的继承吧

  • 所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype,其包含了 JavaScript 中许多通用的功能

  • 为什么能创建 “类”,借助一种特殊的属性:所有的函数默认都会拥有一个名为 prototype 的共有且不可枚举的属性,它会指向另外一个对象,这个对象通常被称为函数的原型


function Person(name) {  this.name = name;}
Person.prototype.constructor = Person
复制代码


  • 在发生 new 构造函数调用时,会将创建的新对象的 [[Prototype]] 链接到 Person.prototype 指向的对象,这个机制就被称为原型链继承

  • 方法定义在原型上,属性定义在构造函数上

  • 首先要说一下 JS 原型和实例的关系:每个构造函数 (constructor)都有一个原型对象(prototype),这个原型对象包含一个指向此构造函数的指针属性,通过 new 进行构造函数调用生成的实例,此实例包含一个指向原型对象的指针,也就是通过 [[Prototype]] 链接到了这个原型对象

  • 然后说一下 JS 中属性的查找:当我们试图引用实例对象的某个属性时,是按照这样的方式去查找的,首先查找实例对象上是否有这个属性,如果没有找到,就去构造这个实例对象的构造函数的 prototype 所指向的对象上去查找,如果还找不到,就从这个 prototype 对象所指向的构造函数的 prototype 原型对象上去查找

  • 什么是原型链:这样逐级查找形似一个链条,且通过 [[Prototype]] 属性链接,所以被称为原型链

  • 什么是原型链继承,类比类的继承:当有两个构造函数 A 和 B,将一个构造函数 A 的原型对象的,通过其 [[Prototype]] 属性链接到另外一个 B 构造函数的原型对象时,这个过程被称之为原型继承。


** 标准答案更正确的解释**


什么是原型链?


当对象查找一个属性的时候,如果没有在自身找到,那么就会查找自身的原型,如果原型还没有找到,那么会继续查找原型的原型,直到找到 Object.prototype 的原型时,此时原型为 null,查找停止。这种通过 通过原型链接的逐级向上的查找链被称为原型链


什么是原型继承?


一个对象可以使用另外一个对象的属性或者方法,就称之为继承。具体是通过将这个对象的原型设置为另外一个对象,这样根据原型链的规则,如果查找一个对象属性且在自身不存在时,就会查找另外一个对象,相当于一个对象可以使用另外一个对象的属性和方法了。

Promise.all 和 Promise.race 的区别的使用场景

(1)Promise.all Promise.all可以将多个Promise实例包装成一个新的 Promise 实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被 reject 失败状态的值


Promise.all 中传入的是数组,返回的也是是数组,并且会将进行映射,传入的 promise 对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。


需要注意,Promise.all 获得的成功结果的数组里面的数据顺序和 Promise.all 接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用 Promise.all 来解决。


(2)Promise.race


顾名思义,Promse.race 就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:


Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
复制代码

实现数组原型方法

forEach

语法:arr.forEach(callback(currentValue [, index [, array]])[, thisArg])

参数:

callback:为数组中每个元素执行的函数,该函数接受 1-3 个参数currentValue: 数组中正在处理的当前元素index(可选): 数组中正在处理的当前元素的索引array(可选): forEach() 方法正在操作的数组 thisArg(可选): 当执行回调函数 callback 时,用作 this 的值。

返回值:undefined


Array.prototype.forEach1 = function(callback, thisArg) {    if(this == null) {        throw new TypeError('this is null or not defined');    }    if(typeof callback !== "function") {        throw new TypeError(callback + 'is not a function');    }    // 创建一个新的 Object 对象。该对象将会包裹(wrapper)传入的参数 this(当前数组)。    const O = Object(this);    // O.length >>> 0 无符号右移 0 位    // 意义:为了保证转换后的值为正整数。    // 其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型    const len = O.length >>> 0;    let k = 0;    while(k < len) {        if(k in O) {            callback.call(thisArg, O[k], k, O);        }        k++;    }}
复制代码

map

语法: arr.map(callback(currentValue [, index [, array]])[, thisArg])

参数:与 forEach() 方法一样

返回值:一个由原数组每个元素执行回调函数的结果组成的新数组。


Array.prototype.map1 = function(callback, thisArg) {    if(this == null) {        throw new TypeError('this is null or not defined');    }    if(typeof callback !== "function") {        throw new TypeError(callback + 'is not a function');    }    const O = Object(this);     const len = O.length >>> 0;    let newArr = [];  // 返回的新数组    let k = 0;    while(k < len) {        if(k in O) {            newArr[k] = callback.call(thisArg, O[k], k, O);        }        k++;    }    return newArr;}
复制代码

filter

语法:arr.filter(callback(element [, index [, array]])[, thisArg])

参数:

callback: 用来测试数组的每个元素的函数。返回 true 表示该元素通过测试,保留该元素,false 则不保留。它接受以下三个参数:element、index、array,参数的意义与 forEach 一样。

thisArg(可选): 执行 callback 时,用于 this 的值。

返回值:一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。


Array.prototype.filter1 = function(callback, thisArg) {    if(this == null) {        throw new TypeError('this is null or not defined');    }    if(typeof callback !== "function") {        throw new TypeError(callback + 'is not a function');    }    const O = Object(this);     const len = O.length >>> 0;    let newArr = [];  // 返回的新数组    let k = 0;    while(k < len) {        if(k in O) {            if(callback.call(thisArg, O[k], k, O)) {                newArr.push(O[k]);            }        }        k++;    }    return newArr;}
复制代码

some

语法:arr.some(callback(element [, index [, array]])[, thisArg])

参数:

callback: 用来测试数组的每个元素的函数。接受以下三个参数:element、index、array,参数的意义与 forEach 一样。

thisArg(可选): 执行 callback 时,用于 this 的值。返回值:数组中有至少一个元素通过回调函数的测试就会返回 true;所有元素都没有通过回调函数的测试返回值才会为 false


Array.prototype.some1 = function(callback, thisArg) {    if(this == null) {        throw new TypeError('this is null or not defined');    }    if(typeof callback !== "function") {        throw new TypeError(callback + 'is not a function');    }    const O = Object(this);     const len = O.length >>> 0;    let k = 0;    while(k < len) {        if(k in O) {            if(callback.call(thisArg, O[k], k, O)) {                return true            }        }        k++;    }    return false;}
复制代码

reduce

语法:arr.reduce(callback(preVal, curVal[, curIndex [, array]])[, initialValue])

参数:

callback: 一个 “reducer” 函数,包含四个参数:

preVal:上一次调用 callback 时的返回值。在第一次调用时,若指定了初始值 initialValue,其值则为 initialValue,否则为数组索引为 0 的元素 array[0]

curVal:数组中正在处理的元素。在第一次调用时,若指定了初始值 initialValue,其值则为数组索引为 0 的元素 array[0],否则为 array[1]

curIndex(可选):数组中正在处理的元素的索引。若指定了初始值 initialValue,则起始索引号为 0,否则从索引 1 起始。

array(可选):用于遍历的数组。initialValue(可选): 作为第一次调用 callback 函数时参数 preVal 的值。若指定了初始值 initialValue,则 curVal 则将使用数组第一个元素;否则 preVal 将使用数组第一个元素,而 curVal 将使用数组第二个元素。返回值:使用 “reducer” 回调函数遍历整个数组后的结果。


Array.prototype.reduce1 = function(callback, initialValue) {    if(this == null) {        throw new TypeError('this is null or not defined');    }    if(typeof callback !== "function") {        throw new TypeError(callback + 'is not a function');    }    const O = Object(this);    const len = O.length >>> 0;    let k = 0;    let accumulator = initialValue;    // 如果第二个参数为undefined的情况下,则数组的第一个有效值(非empty)作为累加器的初始值    if(accumulator === undefined) {        while(k < len && !(k in O)) {            k++;        }        // 如果超出数组界限还没有找到累加器的初始值,则TypeError        if(k >= len) {            throw new TypeError('Reduce of empty array with no initial value');        }        accumulator = O[k++];    }    while(k < len) {        if(k in O) {            accumulator = callback(accumulator, O[k], k, O);        }        k++;    }    return accumulator;}
复制代码

代码输出结果

const promise = new Promise((resolve, reject) => {  console.log(1);  console.log(2);});promise.then(() => {  console.log(3);});console.log(4);
复制代码


输出结果如下:


1 2 4
复制代码


promise.then 是微任务,它会在所有的宏任务执行完之后才会执行,同时需要 promise 内部的状态发生变化,因为这里内部没有发生变化,一直处于 pending 状态,所以不输出 3。

异步任务调度器

描述:实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有 limit 个。


实现


class Scheduler {    queue = [];  // 用队列保存正在执行的任务    runCount = 0;  // 计数正在执行的任务个数    constructor(limit) {        this.maxCount = limit;  // 允许并发的最大个数    }    add(time, data){        const promiseCreator = () => {            return new Promise((resolve, reject) => {                setTimeout(() => {                    console.log(data);                    resolve();                }, time);            });        }        this.queue.push(promiseCreator);        // 每次添加的时候都会尝试去执行任务        this.request();    }    request() {        // 队列中还有任务才会被执行        if(this.queue.length && this.runCount < this.maxCount) {            this.runCount++;            // 执行先加入队列的函数            this.queue.shift()().then(() => {                this.runCount--;                // 尝试进行下一次任务                this.request();            });        }    }}
// 测试const scheduler = new Scheduler(2);const addTask = (time, data) => { scheduler.add(time, data);}
addTask(1000, '1');addTask(500, '2');addTask(300, '3');addTask(400, '4');// 输出结果 2 3 1 4
复制代码

如何判断数组类型

Array.isArray

函数柯里化

什么叫函数柯里化?其实就是将使用多个参数的函数转换成一系列使用一个参数的函数的技术。还不懂?来举个例子。


function add(a, b, c) {    return a + b + c}add(1, 2, 3)let addCurry = curry(add)addCurry(1)(2)(3)
复制代码


现在就是要实现 curry 这个函数,使函数从一次调用传入多个参数变成多次调用每次传一个参数。


function curry(fn) {    let judge = (...args) => {        if (args.length == fn.length) return fn(...args)        return (...arg) => judge(...args, ...arg)    }    return judge}
复制代码

如何获得对象非原型链上的属性?

使用后hasOwnProperty()方法来判断属性是否属于原型链的属性:


function iterate(obj){   var res=[];   for(var key in obj){        if(obj.hasOwnProperty(key))           res.push(key+': '+obj[key]);   }   return res;} 
复制代码

什么是 margin 重叠问题?如何解决?

问题描述: 两个块级元素的上外边距和下外边距可能会合并(折叠)为一个外边距,其大小会取其中外边距值大的那个,这种行为就是外边距折叠。需要注意的是,浮动的元素和绝对定位这种脱离文档流的元素的外边距不会折叠。重叠只会出现在垂直方向


计算原则: 折叠合并后外边距的计算原则如下:


  • 如果两者都是正数,那么就去最大者

  • 如果是一正一负,就会正值减去负值的绝对值

  • 两个都是负值时,用 0 减去两个中绝对值大的那个


解决办法: 对于折叠的情况,主要有两种:兄弟之间重叠父子之间重叠 (1)兄弟之间重叠


  • 底部元素变为行内盒子:display: inline-block

  • 底部元素设置浮动:float

  • 底部元素的 position 的值为absolute/fixed


(2)父子之间重叠


  • 父元素加入:overflow: hidden

  • 父元素添加透明边框:border:1px solid transparent

  • 子元素变为行内盒子:display: inline-block

  • 子元素加入浮动属性或定位

await 到底在等啥?

await 在等待什么呢? 一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。


因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行:


function getSomething() {    return "something";}async function testAsync() {    return Promise.resolve("hello async");}async function test() {    const v1 = await getSomething();    const v2 = await testAsync();    console.log(v1, v2);}test();
复制代码


await 表达式的运算结果取决于它等的是什么。


  • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

  • 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。


来看一个例子:


function testAsy(x){   return new Promise(resolve=>{setTimeout(() => {       resolve(x);     }, 3000)    }   )}async function testAwt(){      let result =  await testAsy('hello world');  console.log(result);    // 3秒钟之后出现hello world  console.log('cuger')   // 3秒钟之后出现cug}testAwt();console.log('cug')  //立即输出cug
复制代码


这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。await 暂停当前 async 的执行,所以'cug''最先输出,hello world'和‘cuger’是 3 秒钟后同时出现的。

用户头像

coder2028

关注

还未添加个人签名 2022.09.08 加入

还未添加个人简介

评论

发布
暂无评论
京东前端常考面试题(附答案)_JavaScript_coder2028_InfoQ写作社区