2023面试官常考的前端面试题

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));
}

Object.assign()

描述Object.assign()方法用于将所有可枚举Object.propertyIsEnumerable() 返回 true)和自有Object.hasOwnProperty() 返回 true)属性的值从一个或多个源对象复制到目标对象。它将返回修改后的目标对象(请注意这个操作是浅拷贝)。

实现

Object.assign = function(target, ...source) {
    if(target == null) {
        throw new TypeError('Cannot convert undefined or null to object');
    }
    let res = Object(target);
    source.forEach(function(obj) {
        if(obj != null) {
            // for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)
            // hasOwnProperty 方法只考虑对象自身的属性
            for(let key in obj) {
                if(obj.hasOwnProperty(key)) {
                    res[key] = obj[key];
                }
            }
        }
    });
    return res;
}

如何防御 XSS 攻击?

可以看到XSS危害如此之大, 那么在开发网站时就要做好防御措施,具体措施如下:

  • 可以从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回(不使用服务端渲染)。另一种是对需要插入到 HTML 中的代码做好充分的转义。对于 DOM 型的攻击,主要是前端脚本的不可靠而造成的,对于数据获取渲染和字符串拼接的时候应该对可能出现的恶意代码情况进行判断。
  • 使用 CSP ,CSP 的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击。
  1. CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。
  2. 通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式
  • 对一些敏感信息进行保护,比如 cookie 使用 http-only,使得脚本无法获取。也可以使用验证码,避免脚本伪装成用户执行一些操作。

代码输出结果

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

输出结果如下:

1
7
6
8
2
4
3
5
9
11
10
12

(1)第一轮事件循环流程分析如下:

  • 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
  • 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。暂且记为setTimeout1
  • 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。记为process1
  • 遇到Promisenew Promise直接执行,输出7。then被分发到微任务Event Queue中。记为then1
  • 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,记为setTimeout2
宏任务Event Queue 微任务Event Queue
setTimeout1 process1
setTimeout2 then1

上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。发现了process1then1两个微任务:

  • 执行process1,输出6。
  • 执行then1,输出8。

第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。

(2)第二轮时间循环从**setTimeout1**宏任务开始:

  • 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2
  • new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2
宏任务Event Queue 微任务Event Queue
setTimeout2 process2
then2

第二轮事件循环宏任务结束,发现有process2then2两个微任务可以执行:

  • 输出3。
  • 输出5。

第二轮事件循环结束,第二轮输出2,4,3,5。

(3)第三轮事件循环开始,此时只剩setTimeout2了,执行。

  • 直接输出9。
  • process.nextTick()分发到微任务Event Queue中。记为process3
  • 直接执行new Promise,输出11。
  • then分发到微任务Event Queue中,记为then3
宏任务Event Queue 微任务Event Queue
process3
then3

第三轮事件循环宏任务执行结束,执行两个微任务process3then3

  • 输出10。
  • 输出12。

第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。

call apply bind

题目描述:手写 call apply bind 实现

实现代码如下:

Function.prototype.myCall = function (context, ...args) {
  if (!context || context === null) {
    context = window;
  }
  // 创造唯一的key值  作为我们构造的context内部方法名
  let fn = Symbol();
  context[fn] = this; //this指向调用call的函数
  // 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了
  return context[fn](...args);
};

// apply原理一致  只是第二个参数是传入的数组
Function.prototype.myApply = function (context, args) {
  if (!context || context === null) {
    context = window;
  }
  // 创造唯一的key值  作为我们构造的context内部方法名
  let fn = Symbol();
  context[fn] = this;
  // 执行函数并返回结果
  return context[fn](...args);
};

//bind实现要复杂一点  因为他考虑的情况比较多 还要涉及到参数合并(类似函数柯里化)

Function.prototype.myBind = function (context, ...args) {
  if (!context || context === null) {
    context = window;
  }
  // 创造唯一的key值  作为我们构造的context内部方法名
  let fn = Symbol();
  context[fn] = this;
  let _this = this;
  //  bind情况要复杂一点
  const result = function (...innerArgs) {
    // 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象
    // 此时由于new操作符作用  this指向result实例对象  而result又继承自传入的_this 根据原型链知识可得出以下结论
    // this.__proto__ === result.prototype   //this instanceof result =>true
    // this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true
    if (this instanceof _this === true) {
      // 此时this指向指向result的实例  这时候不需要改变this指向
      this[fn] = _this;
      this[fn](...[...args, ...innerArgs]); //这里使用es6的方法让bind支持参数合并
    } else {
      // 如果只是作为普通函数调用  那就很简单了 直接改变this指向为传入的context
      context[fn](...[...args, ...innerArgs]);
    }
  };
  // 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
  // 实现继承的方式: 使用Object.create
  result.prototype = Object.create(this.prototype);
  return result;
};

//用法如下

// function Person(name, age) {
//   console.log(name); //'我是参数传进来的name'
//   console.log(age); //'我是参数传进来的age'
//   console.log(this); //构造函数this指向实例对象
// }
// // 构造函数原型的方法
// Person.prototype.say = function() {
//   console.log(123);
// }
// let obj = {
//   objName: '我是obj传进来的name',
//   objAge: '我是obj传进来的age'
// }
// // 普通函数
// function normalFun(name, age) {
//   console.log(name);   //'我是参数传进来的name'
//   console.log(age);   //'我是参数传进来的age'
//   console.log(this); //普通函数this指向绑定bind的第一个参数 也就是例子中的obj
//   console.log(this.objName); //'我是obj传进来的name'
//   console.log(this.objAge); //'我是obj传进来的age'
// }

// 先测试作为构造函数调用
// let bindFun = Person.myBind(obj, '我是参数传进来的name')
// let a = new bindFun('我是参数传进来的age')
// a.say() //123

// 再测试作为普通函数调用
// let bindFun = normalFun.myBind(obj, '我是参数传进来的name')
//  bindFun('我是参数传进来的age')

介绍 Loader

常用 Loader:

  • file-loader: 加载文件资源,如 字体 / 图片 等,具有移动/复制/命名等功能;
  • url-loader: 通常用于加载图片,可以将小图片直接转换为 Date Url,减少请求;
  • babel-loader: 加载 js / jsx 文件, 将 ES6 / ES7 代码转换成 ES5,抹平兼容性问题;
  • ts-loader: 加载 ts / tsx 文件,编译 TypeScript;
  • style-loader: 将 css 代码以