写点什么

社招前端必会面试题(附答案)

作者:loveX001
  • 2022-10-25
    浙江
  • 本文字数:6707 字

    阅读完需:约 22 分钟

箭头函数的 this 指向哪⾥?

箭头函数不同于传统 JavaScript 中的函数,箭头函数并没有属于⾃⼰的 this,它所谓的 this 是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的 this,所以是不会被 new 调⽤的,这个所谓的 this 也不会被改变。


可以⽤Babel 理解⼀下箭头函数:


// ES6 const obj = {   getArrow() {     return () => {       console.log(this === obj);     };   } }
复制代码


转化后:


// ES5,由 Babel 转译var obj = {    getArrow: function getArrow() {      var _this = this;      return function () {         console.log(_this === obj);      };    } };
复制代码

数组去重

ES5 实现:


function unique(arr) {    var res = arr.filter(function(item, index, array) {        return array.indexOf(item) === index    })    return res}
复制代码


ES6 实现:


var unique = arr => [...new Set(arr)]
复制代码

事件传播机制(事件流)

冒泡和捕获

IE 兼容

  • attchEvent('on' + type, handler)

  • detachEvent('on' + type, handler)

代码输出结果

async function async1() {  console.log("async1 start");  await async2();  console.log("async1 end");}async function async2() {  console.log("async2");}async1();console.log('start')
复制代码


输出结果如下:


async1 startasync2startasync1 end
复制代码


代码的执行过程如下:


  1. 首先执行函数中的同步代码async1 start,之后遇到了await,它会阻塞async1后面代码的执行,因此会先去执行async2中的同步代码async2,然后跳出async1

  2. 跳出async1函数后,执行同步代码start

  3. 在一轮宏任务全部执行完之后,再来执行await后面的内容async1 end


这里可以理解为 await 后面的语句相当于放到了 new Promise 中,下一行及之后的语句相当于放在 Promise.then 中。

代码输出结果

async function async1 () {  console.log('async1 start');  await new Promise(resolve => {    console.log('promise1')  })  console.log('async1 success');  return 'async1 end'}console.log('srcipt start')async1().then(res => console.log(res))console.log('srcipt end')
复制代码


输出结果如下:


script startasync1 startpromise1script end
复制代码


这里需要注意的是在async1await后面的 Promise 是没有返回值的,也就是它的状态始终是pending状态,所以在await之后的内容是不会执行的,包括async1后面的 .then

代码输出结果

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 捕获;但并不会影响数组中其它的异步任务的执行。

事件流

事件流是网页元素接收事件的顺序,"DOM2 级事件"规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。首先发生的事件捕获,为截获事件提供机会。然后是实际的目标接受事件。最后一个阶段是时间冒泡阶段,可以在这个阶段对事件做出响应。虽然捕获阶段在规范中规定不允许响应事件,但是实际上还是会执行,所以有两次机会获取到目标对象。


<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title>事件冒泡</title></head><body>    <div>        <p id="parEle">我是父元素    <span id="sonEle">我是子元素</span></p>    </div></body></html><script type="text/javascript">var sonEle = document.getElementById('sonEle');var parEle = document.getElementById('parEle');parEle.addEventListener('click', function () {    alert('父级 冒泡');}, false);parEle.addEventListener('click', function () {    alert('父级 捕获');}, true);sonEle.addEventListener('click', function () {    alert('子级冒泡');}, false);sonEle.addEventListener('click', function () {    alert('子级捕获');}, true);
</script>
复制代码


当容器元素及嵌套元素,即在捕获阶段又在冒泡阶段调用事件处理程序时:事件按 DOM 事件流的顺序执行事件处理程序:


  • 父级捕获

  • 子级捕获

  • 子级冒泡

  • 父级冒泡


且当事件处于目标阶段时,事件调用顺序决定于绑定事件的书写顺序,按上面的例子为,先调用冒泡阶段的事件处理程序,再调用捕获阶段的事件处理程序。依次 alert 出“子集冒泡”,“子集捕获”。

代码输出结果

console.log(1)
setTimeout(() => { console.log(2)})
new Promise(resolve => { console.log(3) resolve(4)}).then(d => console.log(d))
setTimeout(() => { console.log(5) new Promise(resolve => { resolve(6) }).then(d => console.log(d))})
setTimeout(() => { console.log(7)})
console.log(8)
复制代码


输出结果如下:


13842567
复制代码


代码执行过程如下:


  1. 首先执行 script 代码,打印出 1;

  2. 遇到第一个定时器,加入到宏任务队列;

  3. 遇到 Promise,执行代码,打印出 3,遇到 resolve,将其加入到微任务队列;

  4. 遇到第二个定时器,加入到宏任务队列;

  5. 遇到第三个定时器,加入到宏任务队列;

  6. 继续执行 script 代码,打印出 8,第一轮执行结束;

  7. 执行微任务队列,打印出第一个 Promise 的 resolve 结果:4;

  8. 开始执行宏任务队列,执行第一个定时器,打印出 2;

  9. 此时没有微任务,继续执行宏任务中的第二个定时器,首先打印出 5,遇到 Promise,首选打印出 6,遇到 resolve,将其加入到微任务队列;

  10. 执行微任务队列,打印出 6;

  11. 执行宏任务队列中的最后一个定时器,打印出 7。


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

<script src=’xxx’ ’xxx’/>外部 js 文件先加载还是 onload 先执行,为什么?

onload 是所以加载完成之后执行的

如何判断数组类型

Array.isArray

代码输出问题

function Parent() {    this.a = 1;    this.b = [1, 2, this.a];    this.c = { demo: 5 };    this.show = function () {        console.log(this.a , this.b , this.c.demo );    }}
function Child() { this.a = 2; this.change = function () { this.b.push(this.a); this.a = this.b.length; this.c.demo = this.a++; }}
Child.prototype = new Parent();var parent = new Parent();var child1 = new Child();var child2 = new Child();child1.a = 11;child2.a = 12;parent.show();child1.show();child2.show();child1.change();child2.change();parent.show();child1.show();child2.show();
复制代码


输出结果:


parent.show(); // 1  [1,2,1] 5
child1.show(); // 11 [1,2,1] 5child2.show(); // 12 [1,2,1] 5
parent.show(); // 1 [1,2,1] 5
child1.show(); // 5 [1,2,1,11,12] 5
child2.show(); // 6 [1,2,1,11,12] 5
复制代码


这道题目值得神帝,他涉及到的知识点很多,例如 this 的指向、原型、原型链、类的继承、数据类型等。


解析:


  1. parent.show(),可以直接获得所需的值,没啥好说的;

  2. child1.show(),Child的构造函数原本是指向Child的,题目显式将Child类的原型对象指向了Parent类的一个实例,需要注意Child.prototype指向的是Parent的实例parent,而不是指向Parent这个类。

  3. child2.show(),这个也没啥好说的;

  4. parent.show(),parent是一个Parent类的实例,Child.prorotype指向的是Parent类的另一个实例,两者在堆内存中互不影响,所以上述操作不影响parent实例,所以输出结果不变;

  5. child1.show(),child1执行了change()方法后,发生了怎样的变化呢?


  • this.b.push(this.a),由于 this 的动态指向特性,this.b 会指向Child.prototype上的 b 数组,this.a 会指向child1a 属性,所以Child.prototype.b变成了**[1,2,1,11]**;

  • this.a = this.b.length,这条语句中this.athis.b的指向与上一句一致,故结果为child1.a变为 4;

  • this.c.demo = this.a++,由于child1自身属性并没有 c 这个属性,所以此处的this.c会指向Child.prototype.cthis.a值为 4,为原始类型,故赋值操作时会直接赋值,Child.prototype.c.demo的结果为 4,而this.a随后自增为 5(4 + 1 = 5)。


  1. child2执行了change()方法, 而child2child1均是Child类的实例,所以他们的原型链指向同一个原型对象Child.prototype,也就是同一个parent实例,所以child2.change()中所有影响到原型对象的语句都会影响child1的最终输出结果。


  • this.b.push(this.a),由于 this 的动态指向特性,this.b 会指向Child.prototype上的 b 数组,this.a 会指向child2a 属性,所以Child.prototype.b变成了**[1,2,1,11,12]**;

  • this.a = this.b.length,这条语句中this.athis.b的指向与上一句一致,故结果为child2.a变为 5;

  • this.c.demo = this.a++,由于child2自身属性并没有 c 这个属性,所以此处的this.c会指向Child.prototype.c,故执行结果为Child.prototype.c.demo的值变为child2.a的值 5,而child2.a最终自增为 6(5 + 1 = 6)。

代码输出结果

Promise.reject('err!!!')  .then((res) => {    console.log('success', res)  }, (err) => {    console.log('error', err)  }).catch(err => {    console.log('catch', err)  })
复制代码


输出结果如下:


error err!!!
复制代码


我们知道,.then函数中的两个参数:


  • 第一个参数是用来处理 Promise 成功的函数

  • 第二个则是处理失败的函数


也就是说Promise.resolve('1')的值会进入成功的函数,Promise.reject('2')的值会进入失败的函数。


在这道题中,错误直接被then的第二个参数捕获了,所以就不会被catch捕获了,输出结果为:error err!!!'


但是,如果是像下面这样:


Promise.resolve()  .then(function success (res) {    throw new Error('error!!!')  }, function fail1 (err) {    console.log('fail1', err)  }).catch(function fail2 (err) {    console.log('fail2', err)  })
复制代码


then的第一参数中抛出了错误,那么他就不会被第二个参数不活了,而是被后面的catch捕获到。

说一说你用过的 css 布局

gird布局,layout布局,flex布局,双飞翼,圣杯布局等
复制代码

new 一个构造函数,如果函数返回 return {}return nullreturn 1return true 会发生什么情况?

如果函数返回一个对象,那么 new 这个函数调用返回这个函数的返回对象,否则返回 new 创建的新对象

Promise.any

描述:只要 promises 中有一个fulfilled,就返回第一个fulfilledPromise实例的返回值。


实现


Promise.any = function(promises) {    return new Promise((resolve, reject) => {        if(Array.isArray(promises)) {            if(promises.length === 0) return reject(new AggregateError("All promises were rejected"));            let count = 0;            promises.forEach((item, index) => {                Promise.resolve(item).then(                    value => resolve(value),                    reason => {                        count++;                        if(count === promises.length) {                            reject(new AggregateError("All promises were rejected"));                        };                    }                );            })        }        else return reject(new TypeError("Argument is not iterable"));    });}
复制代码

代码输出结果

function a() {  console.log(this);}a.call(null);
复制代码


打印结果:window 对象


根据 ECMAScript262 规范规定:如果第一个参数传入的对象调用者是 null 或者 undefined,call 方法将把全局对象(浏览器上是 window 对象)作为 this 的值。所以,不管传入 null 还是 undefined,其 this 都是全局对象 window。所以,在浏览器上答案是输出 window 对象。


要注意的是,在严格模式中,null 就是 null,undefined 就是 undefined:


'use strict';
function a() { console.log(this);}a.call(null); // nulla.call(undefined); // undefined
复制代码

图片懒加载

实现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);
复制代码

代码输出结果

function fn1(){  console.log('fn1')}var fn2
fn1()fn2()
fn2 = function() { console.log('fn2')}
fn2()
复制代码


输出结果:


fn1Uncaught TypeError: fn2 is not a functionfn2
复制代码


这里也是在考察变量提升,关键在于第一个 fn2(),这时 fn2 仍是一个 undefined 的变量,所以会报错 fn2 不是一个函数。

虚拟 DOM 转换成真实 DOM

描述:将如下 JSON 格式的虚拟 DOM 结构转换成真实 DOM 结构。


// vnode 结构{    tag: 'DIV',    attrs: {        id: "app"    },    children: [        {            tag: 'SPAN',            children: [                {                    tag: 'A',                    children: []                }            ]        }    ]}// 真实DOM 结构<div id="app">    <span>        <a></a>    </span></div>
复制代码


实现


function _render(vnode) {    // 如果是数字类型转化为字符串;    if(typeof vnode === "number") {        vnode = String(vnode);    }    // 字符串类型直接就是文本节点    if(typeof vnode === "string") {        return document.createTextNode(vnode);    }    // 普通 DOM    const dom = document.createElement(vnode.tag);    if(vnode.attrs) {        // 遍历属性        Object.keys(vnode.attrs).forEach((key) => {            dom.setAttribute(key, vnode.attrs[key]);        });    }    // 子数组进行递归操作    vnode.children.forEach((child) => dom.appendChild(_render(child)));    return dom;}
// 测试let vnode = { tag: "DIV", attrs: { id: "app", }, children: [ { tag: "SPAN", children: [ { tag: "A", children: [], }, ], }, ],};console.log(_render(vnode)); // <div id="app"><span><a></a></span></div>
复制代码


用户头像

loveX001

关注

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

还未添加个人简介

评论

发布
暂无评论
社招前端必会面试题(附答案)_JavaScript_loveX001_InfoQ写作社区