关于前端:校招前端二面经典面试题附答案

10次阅读

共计 10859 个字符,预计需要花费 28 分钟才能阅读完成。

代码输入后果

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 后输入
1
3
// 2s 后输入
2
Error: 2
// 4s 后输入
4

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

伪元素和伪类的区别和作用?

  • 伪元素:在内容元素的前后插入额定的元素或款式,然而这些元素实际上并不在文档中生成。它们只在内部显示可见,但不会在文档的源代码中找到它们,因而,称为“伪”元素。例如:
p::before {content:"第一章:";}
p::after {content:"Hot!";}
p::first-line {background:red;}
p::first-letter {font-size:30px;}
  • 伪类:将非凡的成果增加到特定选择器上。它是已有元素上增加类别的,不会产生新的元素。例如:
a:hover {color: #FF00FF}
p:first-child {color: red}

总结: 伪类是通过在元素选择器上加⼊伪类扭转元素状态,⽽伪元素通过对元素的操作进⾏对元素的扭转。

代码输入后果

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。

箭头函数和一般函数有什么区别?

(1)箭头函数比一般函数更加简洁
    如果没有参数,就间接写一个空括号即可
    如果只有一个参数,能够省去参数括号
    如果有多个参数,用逗号宰割
    如果函数体的返回值只有一句,能够省略大括号
    如果函数体不须要返回值,且只有一句话,能够给这个语句后面加一个 void 关键字。最罕用的就是调用一个函数:let fn = () => void doesNotReturn()

 (2) 箭头函数没有本人的 this
 箭头函数不会创立本人的 this, 所以它没有本人的 this, 它只会在本人作用域的上一层继承 this。所以箭头函数中的 this 的指向在它在定义时一家确定了,之后不会扭转。(3)箭头函数继承来的 this 指向永远不会扭转

 (4) call()、apply()、bind()等办法不能扭转箭头函数中的 this 指向 

 (5) 箭头函数不能作为构造函数应用

 (6) 箭头函数没有本人的 arguments

 (7) 箭头函数没有 prototype

 (8) 箭头函数不能用作 Generator 函数, 不能应用 yeild 关键字

归并排序 – 工夫复杂度 nlog(n)

题目形容: 实现一个工夫复杂度为 nlog(n)的排序算法

实现代码如下:

function merge(left, right) {let res = [];
  let i = 0;
  let j = 0;
  while (i < left.length && j < right.length) {if (left[i] < right[j]) {res.push(left[i]);
      i++;
    } else {res.push(right[j]);
      j++;
    }
  }
  if (i < left.length) {res.push(...left.slice(i));
  } else {res.push(...right.slice(j));
  }
  return res;
}

function mergeSort(arr) {if (arr.length < 2) {return arr;}
  const mid = Math.floor(arr.length / 2);

  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  return merge(left, right);
}
// console.log(mergeSort([3, 6, 2, 4, 1]));

列表转成树形构造

题目形容:

[
    {
        id: 1,
        text: '节点 1',
        parentId: 0 // 这里用 0 示意为顶级节点
    },
    {
        id: 2,
        text: '节点 1_1',
        parentId: 1 // 通过这个字段来确定子父级
    }
    ...
]

转成
[
    {
        id: 1,
        text: '节点 1',
        parentId: 0,
        children: [
            {
                id:2,
                text: '节点 1_1',
                parentId:1
            }
        ]
    }
]

实现代码如下:

function listToTree(data) {let temp = {};
  let treeData = [];
  for (let i = 0; i < data.length; i++) {temp[data[i].id] = data[i];
  }
  for (let i in temp) {if (+temp[i].parentId != 0) {if (!temp[temp[i].parentId].children) {temp[temp[i].parentId].children = [];}
      temp[temp[i].parentId].children.push(temp[i]);
    } else {treeData.push(temp[i]);
    }
  }
  return treeData;
}

参考 前端进阶面试题具体解答

10 个 Ajax 同时发动申请,全副返回展现后果,并且至少容许三次失败,说出设计思路

这个问题置信很多人会第一工夫想到 Promise.all,然而这个函数有一个局限在于如果失败一次就返回了,间接这样实现会有点问题,须要变通下。以下是两种实现思路

// 以下是不残缺代码,着重于思路 非 Promise 写法
let successCount = 0
let errorCount = 0
let datas = []
ajax(url, (res) => {if (success) {
         success++
         if (success + errorCount === 10) {console.log(datas)
         } else {datas.push(res.data)
         }
     } else {
         errorCount++
         if (errorCount > 3) {
            // 失败次数大于 3 次就应该报错了
             throw Error('失败三次')
         }
     }
})
// Promise 写法
let errorCount = 0
let p = new Promise((resolve, reject) => {if (success) {resolve(res.data)
     } else {
         errorCount++
         if (errorCount > 3) {
            // 失败次数大于 3 次就应该报错了
            reject(error)
         } else {resolve(error)
         }
     }
})
Promise.all([p]).then(v => {console.log(v);
});

let 闭包

let 会产生临时性死区,在以后的执行上下文中,会进行变量晋升,然而未被初始化,所以在执行上下文执行阶段,执行代码如果还没有执行到变量赋值,就援用此变量就会报错,此变量未初始化。

公布订阅模式(事件总线)

形容:实现一个公布订阅模式,领有 on, emit, once, off 办法

class EventEmitter {constructor() {
        // 蕴含所有监听器函数的容器对象
        // 内部结构: {msg1: [listener1, listener2], msg2: [listener3]}
        this.cache = {};}
    // 实现订阅
    on(name, callback) {if(this.cache[name]) {this.cache[name].push(callback);
        }
        else {this.cache[name] = [callback];
        }
    }
    // 删除订阅
    off(name, callback) {if(this.cache[name]) {this.cache[name] = this.cache[name].filter(item => item !== callback);
        }
        if(this.cache[name].length === 0) delete this.cache[name];
    }
    // 只执行一次订阅事件
    once(name, callback) {callback();
        this.off(name, callback);
    }
    // 触发事件
    emit(name, ...data) {if(this.cache[name]) {
            // 创立正本,如果回调函数内持续注册雷同事件,会造成死循环
            let tasks = this.cache[name].slice();
            for(let fn of tasks) {fn(...data);
            }
        }
    }
}

对原型、原型链的了解

在 JavaScript 中是应用构造函数来新建一个对象的,每一个构造函数的外部都有一个 prototype 属性,它的属性值是一个对象,这个对象蕴含了能够由该构造函数的所有实例共享的属性和办法。当应用构造函数新建一个对象后,在这个对象的外部将蕴含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该可能获取到这个值的,然而当初浏览器中都实现了 proto 属性来拜访这个属性,然而最好不要应用这个属性,因为它不是标准中规定的。ES5 中新增了一个 Object.getPrototypeOf() 办法,能够通过这个办法来获取对象的原型。

当拜访一个对象的属性时,如果这个对象外部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有本人的原型,于是就这样始终找上来,也就是原型链的概念。原型链的止境一般来说都是 Object.prototype 所以这就是新建的对象为什么可能应用 toString() 等办法的起因。

特点: JavaScript 对象是通过援用来传递的,创立的每个新对象实体中并没有一份属于本人的原型正本。当批改原型时,与之相干的对象也会继承这一扭转。

GET 和 POST 的申请的区别

Post 和 Get 是 HTTP 申请的两种办法,其区别如下:

  • 利用场景: GET 申请是一个幂等的申请,个别 Get 申请用于对服务器资源不会产生影响的场景,比如说申请一个网页的资源。而 Post 不是一个幂等的申请,个别用于对服务器资源会产生影响的情景,比方注册用户这一类的操作。
  • 是否缓存: 因为两者利用场景不同,浏览器个别会对 Get 申请缓存,但很少对 Post 申请缓存。
  • 发送的报文格式: Get 申请的报文中实体局部为空,Post 申请的报文中实体局部个别为向服务器发送的数据。
  • 安全性: Get 申请能够将申请的参数放入 url 中向服务器发送,这样的做法绝对于 Post 申请来说是不太平安的,因为申请的 url 会被保留在历史记录中。
  • 申请长度: 浏览器因为对 url 长度的限度,所以会影响 get 申请发送数据时的长度。这个限度是浏览器规定的,并不是 RFC 规定的。
  • 参数类型: post 的参数传递反对更多的数据类型。

字符串模板

function render(template, data) {const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
    if (reg.test(template)) { // 判断模板里是否有模板字符串
        const name = reg.exec(template)[1]; // 查找以后模板里第一个模板字符串的字段
        template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
        return render(template, data); // 递归的渲染并返回渲染后的构造
    }
    return template; // 如果模板没有模板字符串间接返回
}

测试:

let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let person = {
    name: '布兰',
    age: 12
}
render(template, person); // 我是布兰,年龄 12,性别 undefined

谈谈你对状态治理的了解

  • 首先介绍 Flux,Flux 是一种应用单向数据流的模式来组合 React 组件的利用架构。
  • Flux 蕴含了 4 个局部,别离是 DispatcherStoreViewActionStore 存储了视图层所有的数据,当 Store 变动后会引起 View 层的更新。如果在视图层触发一个 Action,就会使以后的页面数据值发生变化。Action 会被 Dispatcher 进行对立的收发解决,传递给 Store 层,Store 层曾经注册过相干 Action 的解决逻辑,解决对应的外部状态变动后,触发 View 层更新。
  • Flux 的长处是单向数据流,解决了 MVC 中数据流向不清的问题,使开发者能够疾速理解利用行为。从我的项目构造上简化了视图层设计,明确了分工,数据与业务逻辑也对立寄存治理,使在大型架构的我的项目中更容易治理、保护代码。
  • 其次是 Redux,Redux 自身是一个 JavaScript 状态容器,提供可预测化状态的治理。社区通常认为 Redux 是 Flux 的一个简化设计版本,它提供的状态治理,简化了一些高级个性的实现老本,比方撤销、重做、实时编辑、工夫旅行、服务端同构等。
  • Redux 的外围设计蕴含了三大准则:繁多数据源、纯函数 Reducer、State 是只读的
  • Redux 中整个数据流的计划与 Flux 大同小异
  • Redux 中的另一大外围点是解决“副作用”,AJAX 申请等异步工作,或不是纯函数产生的第三方的交互都被认为是“副作用”。这就造成在纯函数设计的 Redux 中,解决副作用变成了一件至关重要的事件。社区通常有两种解决方案:

    • 第一类是在 Dispatch 的时候会有一个 middleware 中间件层,拦挡散发的 Action 并增加额定的简单行为,还能够增加副作用。第一类计划的风行框架有 Redux-thunk、Redux-Promise、Redux-Observable、Redux-Saga 等。
    • 第二类是容许 Reducer 层中间接解决副作用,采取该计划的有 React LoopReact Loop 在实现中采纳了 Elm 中分形的思维,使代码具备更强的组合能力。
    • 除此以外,社区还提供了更为工程化的计划,比方 rematch 或 dva,提供了更具体的模块架构能力,提供了拓展插件以反对更多功能。
  • Redux 的长处很多:

    • 后果可预测;
    • 代码构造严格易保护;
    • 模块拆散清晰且小函数构造容易编写单元测试;
    • Action 触发的形式,能够在调试器中应用工夫回溯,定位问题更简略快捷;
    • 繁多数据源使服务端同构变得更为容易;社区计划多,生态也更为凋敝。
  • 最初是 Mobx,Mobx 通过监听数据的属性变动,能够间接在数据上更改触发 UI 的渲染。在应用上更靠近 Vue,比起 Flux 与 Redux 的手动挡的体验,更像开自动挡的汽车。Mobx 的响应式实现原理与 Vue 雷同 ,以 Mobx 5 为分界点,5 以前采纳 Object.defineProperty 的计划,5 及当前应用 Proxy 的计划。 它的长处是样板代码少、简略粗犷、用户学习快、响应式自动更新数据 让开发者的心智累赘更低。
  • Mobx 在开发我的项目时简略疾速,但利用 Mobx 的场景,其实齐全能够用 Vue 取代。如果纯用 Vue,体积还会更玲珑

async/await 比照 Promise 的劣势

  • 代码读起来更加同步,Promise 尽管解脱了回调天堂,然而 then 的链式调⽤也会带来额定的浏览累赘
  • Promise 传递两头值⾮常麻烦,⽽ async/await ⼏乎是同步的写法,⾮常优雅
  • 错误处理敌对,async/await 能够⽤成熟的 try/catch,Promise 的谬误捕捉⾮常冗余
  • 调试敌对,Promise 的调试很差,因为没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then 代码块中使⽤调试器的步进 (step-over) 性能,调试器并不会进⼊后续的.then 代码块,因为调试器只能跟踪同步代码的每⼀步。

对执行上下文的了解

1. 执行上下文类型

(1)全局执行上下文

任何不在函数外部的都是全局执行上下文,它首先会创立一个全局的 window 对象,并且设置 this 的值等于这个全局对象,一个程序中只有一个全局执行上下文。

(2)函数执行上下文

当一个函数被调用时,就会为该函数创立一个新的执行上下文,函数的上下文能够有任意多个。

(3)eval函数执行上下文

执行在 eval 函数中的代码会有属于他本人的执行上下文,不过 eval 函数不常应用,不做介绍。

2. 执行上下文栈
  • JavaScript 引擎应用执行上下文栈来治理执行上下文
  • 当 JavaScript 执行代码时,首先遇到全局代码,会创立一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创立一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行实现之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行结束之后,从栈中弹出全局执行上下文。
let a = 'Hello World!';
function first() {console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {console.log('Inside second function');
}
first();
// 执行程序
// 先执行 second(), 在执行 first()
3. 创立执行上下文

创立执行上下文有两个阶段:创立阶段 执行阶段

1)创立阶段

(1)this 绑定

  • 在全局执行上下文中,this 指向全局对象(window 对象)
  • 在函数执行上下文中,this 指向取决于函数如何调用。如果它被一个援用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined

(2)创立词法环境组件

  • 词法环境是一种有 标识符——变量映射 的数据结构,标识符是指变量 / 函数名,变量是对理论对象或原始数据的援用。
  • 词法环境的外部有两个组件:加粗款式 :环境记录器: 用来贮存变量个函数申明的理论地位 外部环境的援用:能够拜访父级作用域

(3)创立变量环境组件

  • 变量环境也是一个词法环境,其环境记录器持有变量申明语句在执行上下文中创立的绑定关系。

2)执行阶段 此阶段会实现对变量的调配,最初执行完代码。

简略来说执行上下文就是指:

在执行一点 JS 代码之前,须要先解析代码。解析的时候会先创立一个全局执行上下文环境,先把代码中行将执行的变量、函数申明都拿进去,变量先赋值为 undefined,函数先申明好可应用。这一步执行完了,才开始正式的执行程序。

在一个函数执行之前,也会创立一个函数执行上下文环境,跟全局执行上下文相似,不过函数执行上下文会多出 this、arguments 和函数的参数。

  • 全局上下文:变量定义,函数申明
  • 函数上下文:变量定义,函数申明,thisarguments

instanceof

作用:判断对象的具体类型。能够区别 arrayobjectnullobject 等。

语法A instanceof B

如何判断的?: 如果 B 函数的显式原型对象在 A 对象的原型链上,返回true,否则返回false

留神:如果检测原始值,则始终返回 false

实现:

function myinstanceof(left, right) {
    // 根本数据类型都返回 false,留神 typeof 函数 返回 "function"
    if((typeof left !== "object" && typeof left !== "function") || left === null) return false;
    let leftPro = left.__proto__;  // 取右边的(隐式)原型 __proto__
    // left.__proto__ 等价于 Object.getPrototypeOf(left)
    while(true) {
        // 判断是否到原型链顶端
        if(leftPro === null) return false;
        // 判断左边的显式原型 prototype 对象是否在右边的原型链上
        if(leftPro === right.prototype) return true;
        // 原型链查找
        leftPro = leftPro.__proto__;
    }
}

大数相加

题目形容: 实现一个 add 办法实现两个大数相加

let a = "9007199254740991";
let b = "1234567899999999999";

function add(a ,b){//...}

实现代码如下:

function add(a ,b){
   // 取两个数字的最大长度
   let maxLength = Math.max(a.length, b.length);
   // 用 0 去补齐长度
   a = a.padStart(maxLength , 0);//"0009007199254740991"
   b = b.padStart(maxLength , 0);//"1234567899999999999"
   // 定义加法过程中须要用到的变量
   let t = 0;
   let f = 0;   //"进位"
   let sum = "";
   for(let i=maxLength-1 ; i>=0 ; i--){t = parseInt(a[i]) + parseInt(b[i]) + f;
      f = Math.floor(t/10);
      sum = t%10 + sum;
   }
   if(f!==0){sum = '' + f + sum;}
   return sum;
}

li 与 li 之间有看不见的空白距离是什么起因引起的?如何解决?

浏览器会把 inline 内联元素间的空白字符(空格、换行、Tab 等)渲染成一个空格。为了好看,通常是一个 <li> 放在一行,这导致 <li> 换行后产生换行字符,它变成一个空格,占用了一个字符的宽度。

解决办法:

(1)为 <li> 设置 float:left。有余:有些容器是不能设置浮动,如左右切换的焦点图等。

(2)将所有 <li> 写在同一行。有余:代码不美观。

(3)将 <ul> 内的字符尺寸间接设为 0,即 font-size:0。有余:<ul>中的其余字符尺寸也被设为 0,须要额定从新设定其余字符尺寸,且在 Safari 浏览器仍然会呈现空白距离。

(4)打消 <ul> 的字符距离 letter-spacing:-8px,有余:这也设置了 <li> 内的字符距离,因而须要将 <li> 内的字符距离设为默认 letter-spacing:normal。

isNaN 和 Number.isNaN 函数的区别?

  • 函数 isNaN 接管参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因而非数字值传入也会返回 true,会影响 NaN 的判断。
  • 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再持续判断是否为 NaN,不会进行数据类型的转换,这种办法对于 NaN 的判断更为精确。

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 秒钟后同时呈现的。

正文完
 0