2021中高级前端面试题及解析

1.对闭包的看法,为什么要用闭包?说一下闭包原理以及应用场景

1)什么是闭包

函数执行后返回结果是一个内部函数,并被外部变量所引用,如果内部函数持有被执行函数作用域的变量,即形成了闭包。

可以在内部函数访问到外部函数作用域。使用闭包,一可以读取函数中的变量,二可以将函数中的变量存储在内存中,保护变量不被污染。而正因闭包会把函数中的变量值存储在内存中,会对内存有消耗,所以不能滥用闭包,否则会影响网页性能,造成内存泄漏。当不需要使用闭包时,要及时释放内存,可将内层函数对象的变量赋值为null。

2)闭包原理

函数执行分成两个阶段(预编译阶段和执行阶段)。

在预编译阶段,如果发现内部函数使用了外部函数的变量,则会在内存中创建一个“闭包”对象并保存对应变量值,如果已存在“闭包”,则只需要增加对应属性值即可。
执行完后,函数执行上下文会被销毁,函数对“闭包”对象的引用也会被销毁,但其内部函数还持用该“闭包”的引用,所以内部函数可以继续使用“外部函数”中的变量
利用了函数作用域链的特性,一个函数内部定义的函数会将包含外部函数的活动对象添加到它的作用域链中,函数执行完毕,其执行作用域链销毁,但因内部函数的作用域链仍然在引用这个活动对象,所以其活动对象不会被销毁,直到内部函数被烧毁后才被销毁。

3)优点

可以从内部函数访问外部函数的作用域中的变量,且访问到的变量长期驻扎在内存中,可供之后使用
避免变量污染全局
把变量存到独立的作用域,作为私有成员存在

4)缺点

对内存消耗有负面影响。因内部函数保存了对外部变量的引用,导致无法被垃圾回收,增大内存使用量,所以使用不当会导致内存泄漏
对处理速度具有负面影响。闭包的层级决定了引用的外部变量在查找时经过的作用域链长度
可能获取到意外的值(captured value)

4)应用场景

应用场景一: 典型应用是模块封装,在各模块规范出现之前,都是用这样的方式防止变量污染全局。

var Yideng = (function () {
// 这样声明为模块私有变量,外界无法直接访问
var foo = 0;

function Yideng() {}
Yideng.prototype.bar = function bar() {
    return foo;
};
return Yideng;

}());
应用场景二: 在循环中创建闭包,防止取到意外的值。

如下代码,无论哪个元素触发事件,都会弹出 3。因为函数执行后引用的 i 是同一个,而 i 在循环结束后就是 3

for (var i = 0; i < 3; i++) {
document.getElementById(‘id’ + i).onfocus = function() {
alert(i);
};
}
//可用闭包解决
function makeCallback(num) {
return function() {
alert(num);
};
}
for (var i = 0; i < 3; i++) {
document.getElementById(‘id’ + i).onfocus = makeCallback(i);
}

2.说一下 Http 缓存策略,有什么区别,分别解决了什么问题

1)浏览器缓存策略

浏览器每次发起请求时,先在本地缓存中查找结果以及缓存标识,根据缓存标识来判断是否使用本地缓存。如果缓存有效,则使用本地缓存;否则,则向服务器发起请求并携带缓存标识。根据是否需向服务器发起HTTP请求,将缓存过程划分为两个部分:

强制缓存和协商缓存,强缓优先于协商缓存。

  • 强缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行比较缓存策略。
  • 协商缓存,让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,将缓存信息中的EtagLast-Modified通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。

2)强缓存

强缓存命中则直接读取浏览器本地的资源,在network中显示的是from memory或者from disk控制强制缓存的字段有:Cache-Control(http1.1)和Expires(http1.0)
Cache-control是一个相对时间,用以表达自上次请求正确的资源之后的多少秒的时间段内缓存有效。
Expires是一个绝对时间。用以表达在这个时间点之前发起请求可以直接从浏览器中读取数据,而无需发起请求。
Cache-Control的优先级比Expires的优先级高。前者的出现是为了解决Expires在浏览器时间被手动更改导致缓存判断错误的问题。
如果同时存在则使用Cache-control。

5)协商缓存

协商缓存的状态码由服务器决策返回200或者304。
当浏览器的强缓存失效的时候或者请求头中设置了不走强缓存,并且在请求头中设置了If-Modified-Since 或者 If-None-Match 的时候,会将这两个属性值到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回 304 状态,加载浏览器缓存,并且响应头会设置 Last-Modified 或者 ETag 属性。
对比缓存在请求数上和没有缓存是一致的,但如果是 304 的话,返回的仅仅是一个状态码而已,并没有实际的文件内容,因此 在响应体体积上的节省是它的优化点。
协商缓存有 2 组字段(不是两个),控制协商缓存的字段有:Last-Modified/If-Modified-since(http1.0)和Etag/If-None-match(http1.1)。
Last-Modified/If-Modified-since表示的是服务器的资源最后一次修改的时间;Etag/If-None-match表示的是服务器资源的唯一标识,只要资源变化,Etag就会重新生成。
Etag/If-None-match的优先级比Last-Modified/If-Modified-since高。

3.介绍防抖节流原理、区别以及应用,并用JavaScript进行实现。

1)防抖

原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
适用场景:

  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 搜索框联想场景:防止联想发送请求,只发送最后一次输入

实现代码:

function debounce(func, wait) {
    let timeout;
    return function () {
        const context = this;
        const args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

2)节流

原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
适用场景

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
  • 缩放场景:监控浏览器resize 使用时间戳实现
    实现代码:
function throttle(func, wait) {
  let timeout;
  return function () {
    const context = this;
    const args = arguments;
    if (!timeout) {
      timeout = setTimeout(function () {
        timeout = null;
        func.apply(context, args)
      }, wait)
    }
  }
}

4.css 伪类与伪元素区别.

1)伪类(pseudo-classes)

其核⼼就是⽤来选择DOM树之外的信息,不能够被普通选择器选择的⽂档之外的元素,⽤来添加⼀些选择器的特殊效果。
⽐如:hover :active :visited :link :visited :first-child :focus :lang等
由于状态的变化是⾮静态的,所以元素达到⼀个特定状态时,它可能得到⼀个伪类的样式;当状态改变时,它⼜会失去这个样式。
由此可以看出,它的功能和class有些类似,但它是基于⽂档之外的抽象,所以叫 伪类。

2)伪元素(Pseudo-elements)

  • DOM树没有定义的虚拟元素
  • 核⼼就是需要创建通常不存在于⽂档中的元素,⽐如::before ::after它选择的是元素指定内容,表示选择元素内容的之前内容或之后内容。
  • 伪元素控制的内容和元素是没有差别的,但是它本身只是基于元素的抽象,并不存在于⽂档中,所以称为伪元素。⽤于将特殊的效果添加到某些选择器

3)伪类与伪元素的区别

表示⽅法:

  • CSS2 中伪类、伪元素都是以单冒号:表示, CSS2.1 后规定伪类⽤单冒号表示,伪元素⽤双冒号:
  • CSS3中,伪类与伪元素在语法上也有所区别,伪元素修改为以::开头。浏览器对以:开头的伪元素也继续⽀持,但建议规范书写为::开头

定义不同:

伪类即假的类,可以添加类来达到效果
伪元素即假元素,需要通过添加元素才能达到效果
总结:

  • 伪类和伪元素都是⽤来表示⽂档树以外的"元素"。
  • 伪类和伪元素分别⽤单冒号:和双冒号::来表示。
  • 伪类和伪元素的区别,关键点在于如果没有伪元素(或伪类), 是否需要添加元素才能达到效果,如果是则是伪元素,反之则是伪类。

4)相同之处:

伪类和伪元素都不出现在源⽂件和DOM树中。也就是说在html源⽂件中是看不到伪类和伪元素的。
不同之处:
伪类其实就是基于普通DOM元素⽽产⽣的不同状态,他是DOM元素的某⼀特征。
伪元素能够创建在DOM树中不存在的抽象对象,⽽且这些抽象对象是能够访问到的。

5.类数组和数组的区别,dom 的类数组如何转换成数组

1)定义

数组是一个特殊对象,与常规对象的区别:
当由新元素添加到列表中时,自动更新length属性
设置length属性,可以截断数组
从Array.protoype中继承了方法
属性为’Array’
类数组是一个拥有length属性,并且他属性为非负整数的普通对象,类数组不能直接调用数组方法。

2)区别

本质:类数组是简单对象,它的原型关系与数组不同。

// 原型关系和原始值转换
let arrayLike = {
length: 10,
};
console.log(arrayLike instanceof Array); // false
console.log(arrayLike.proto.constructor === Array); // false
console.log(arrayLike.toString()); // [object Object]
console.log(arrayLike.valueOf()); // {length: 10}

let array = [];
console.log(array instanceof Array); // true
console.log(array.proto.constructor === Array); // true
console.log(array.toString()); // ‘’
console.log(array.valueOf()); // []

3)类数组转换为数组

转换方法
使用 Array.from()
使用 Array.prototype.slice.call()
使用 Array.prototype.forEach() 进行属性遍历并组成新的数组
转换须知
转换后的数组长度由 length 属性决定。索引不连续时转换结果是连续的,会自动补位。

6.webpack 做过哪些优化,开发效率方面、打包策略方面等等。

1)优化 Webpack 的构建速度

  • 使用高版本的 Webpack (使用webpack4)
  • 多线程/多实例构建:HappyPack(不维护了)、thread-loader
  • 缩小打包作用域:
  • exclude/include (确定 loader 规则范围)
  • resolve.modules 指明第三方模块的绝对路径(减少不必要的查找)
  • resolve.extensions 尽可能减少后缀尝试的可能性
  • noParse 对完全不需要解析的库进行忽略(不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
  • IgnorePlugin (完全排除模块) 合理使用alias
  • 充分利用缓存提升二次构建速度:
    babel-loader 开启缓存
    terser-webpack-plugin 开启缓存
    使用 cache-loader 或者 hard-source-webpack-plugin
    注意:thread-loader 和 cache-loader 兩個要一起使用的話,請先放 cache-loader 接著是 thread-loader 最後才是 heavy-loader

2)优化 Webpack 的打包体积

压缩代码

  • webpack-paralle-uglify-plugin
  • uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
  • terser-webpack-plugin 开启 parallel 参数

多进程并行压缩
通过 mini-css-extract-plugin 提取 Chunk 中的 CSS 代码到单独文件,通过optimize-css-assets-webpack-plugin插件 开启 cssnano 压缩 CSS。
提取页面公共资源:
使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
基础包分离:将一些基础库放到cdn,比如vue,webpack 配置 external是的vue不打入bundle
Tree shaking
purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)
打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率
禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码
Scope hoisting
构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法,为了充分发挥 Scope hoisting 的作用,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法
图片压缩
使用基于 Node 库的 imagemin (很多定制选项、可以处理多种图片格式)
配置 image-webpack-loader
动态Polyfill
建议采用 polyfill-service 只给用户返回需要的polyfill,社区维护。(部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)
@babel-preset-env 中通过useBuiltIns: 'usage参数来动态加载polyfill。

7.介绍下 promise 的特性、优缺点,内部是如何实现的,动手实现.

1)Promise基本特性

1、Promise有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)
2、Promise对象接受一个回调函数作为参数, 该回调函数接受两个参数,分别是成功时的回调resolve和失败时的回调reject;另外resolve的参数除了正常值以外, 还可能是一个Promise对象的实例;reject的参数通常是一个Error对象的实例。
3、then方法返回一个新的Promise实例,并接收两个参数onResolved(fulfilled状态的回调);onRejected(rejected状态的回调,该参数可选)
4、catch方法返回一个新的Promise实例
5、finally方法不管Promise状态如何都会执行,该方法的回调函数不接受任何参数
6、Promise.all()方法将多个多个Promise实例,包装成一个新的Promise实例,该方法接受一个由Promise对象组成的数组作为参数(Promise.all()方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是Promise实例),注意参数中只要有一个实例触发catch方法,都会触发Promise.all()方法返回的新的实例的catch方法,如果参数中的某个实例本身调用了catch方法,将不会触发Promise.all()方法返回的新实例的catch方法
7、Promise.race()方法的参数与Promise.all方法一样,参数中的实例只要有一个率先改变状态就会将该实例的状态传给Promise.race()方法,并将返回值作为Promise.race()方法产生的Promise实例的返回值
8、Promise.resolve()将现有对象转为Promise对象,如果该方法的参数为一个Promise对象,Promise.resolve()将不做任何处理;如果参数thenable对象(即具有then方法),Promise.resolve()将该对象转为Promise对象并立即执行then方法;如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为fulfilled,其参数将会作为then方法中onResolved回调函数的参数,如果Promise.resolve方法不带参数,会直接返回一个fulfilled状态的 Promise 对象。需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。
9、Promise.reject()同样返回一个新的Promise对象,状态为rejected,无论传入任何参数都将作为reject()的参数

2)Promise优点

①统一异步 API
Promise 的一个重要优点是它将逐渐被用作浏览器的异步 API ,统一现在各种各样的 API ,以及不兼容的模式和手法。
②Promise 与事件对比
和事件相比较, Promise 更适合处理一次性的结果。在结果计算出来之前或之后注册回调函数都是可以的,都可以拿到正确的值。 Promise 的这个优点很自然。但是,不能使用 Promise 处理多次触发的事件。链式处理是 Promise 的又一优点,但是事件却不能这样链式处理。
③Promise 与回调对比
解决了回调地狱的问题,将异步操作以同步操作的流程表达出来。
④Promise 带来的额外好处
包含了更好的错误处理方式(包含了异常处理),并且写起来很轻松(因为可以重用一些同步的工具,比如 Array.prototype.map() )。

3)Promise缺点

1、无法取消Promise,一旦新建它就会立即执行,无法中途取消。
2、如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
3、当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
4、Promise 真正执行回调的时候,定义 Promise 那部分实际上已经走完了,所以 Promise 的报错堆栈上下文不太友好。

4)简单代码实现

最简单的Promise实现有7个主要属性, state(状态), value(成功返回值), reason(错误信息), resolve方法, reject方法, then方法.
参考 https://github.com/lgwebdream/FE-Interview/issues/29

7. 手写发布订阅者模式

 //事件触发器
        class emitEmitter {
            constructor() {
                //subs = {click (eventType): [fn1, fn2]  (handeler), hover: [fn]}
                this.subs = Object.create(null)
            }

            //注册事件
            $on(evenType, handeler) {
                this.subs[evenType] = this.subs[evenType] || []
                this.subs[evenType].push(handeler)
            }

            //触发事件
            $emit(evenType) {
                if (this.subs[evenType]) {
                    this.subs[evenType].forEach(handeler => {
                        handeler()
                    });
                }
            }
        }

        //实现发布订阅者模式自定义事件
        let em = new emitEmitter()

        em.$on('click', () => {
            console.log('click1')
        })
        em.$on('click', () => {
            console.log('click2')
        })

        em.$emit('click')

8.手写观察者模式

//发布者-目标
        class Dep {
            constructor() {
                this.subs = [];
            }
            //添加订阅者
            addSub(sub) {
                if (sub && sub.update) {
                    this.subs.push(sub)
                }
            }
            //通知
            notify() {
                this.subs.forEach(sub => {
                    sub.update()
                })
            }
        }
        //订阅者
        class Watcher {
            update() {
                console.log('update')
            }
        }
        let dep = new Dep()
        let watcher = new Watcher()
        dep.addSub(watcher)
        dep.notify()

9.请写出下面代码执行的的结果

console.log(1);
setTimeout(() => {
  console.log(2);
  process.nextTick(() => {
    console.log(3);
  });
  new Promise((resolve) => {
    console.log(4);
    resolve();
  }).then(() => {
    console.log(5);
  });
});
new Promise((resolve) => {
  console.log(7);
  resolve();
}).then(() => {
  console.log(8);
});
process.nextTick(() => {
  console.log(6);
});
setTimeout(() => {
  console.log(9);
  process.nextTick(() => {
    console.log(10);
  });
  new Promise((resolve) => {
    console.log(11);
    resolve();
  }).then(() => {
    console.log(12);
  });
});

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值