写点什么

字节前端必会面试题

  • 2022 年 9 月 09 日
    浙江
  • 本文字数:7571 字

    阅读完需:约 25 分钟

哪些情况会导致内存泄漏

1、意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收2、被遗忘的计时器或回调函数:设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。3、脱离 DOM 的引用:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。4、闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中。
复制代码


----问题知识点分割线----

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

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


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


----问题知识点分割线----

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

  • 伪元素:在内容元素的前后插入额外的元素或样式,但是这些元素实际上并不在文档中生成。它们只在外部显示可见,但不会在文档的源代码中找到它们,因此,称为“伪”元素。例如:


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}
复制代码


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


----问题知识点分割线----

head 标签有什么作用,其中什么标签必不可少?

标签用于定义文档的头部,它是所有头部元素的容器。 中的元素可以引用脚本、指示浏览器在哪里找到样式表、提供元信息等。


文档的头部描述了文档的各种属性和信息,包括文档的标题、在 Web 中的位置以及和其他文档的关系等。绝大多数文档头部包含的数据都不会真正作为内容显示给读者。


下面这些标签可用在 head 部分:<base>, <link>, <meta>, <script>, <style>, <title>


其中 <title> 定义文档的标题,它是 head 部分中唯一必需的元素。


----问题知识点分割线----

深拷贝

实现一:不考虑 Symbol


function deepClone(obj) {    if(!isObject(obj)) return obj;    let newObj = Array.isArray(obj) ? [] : {};    // for...in 只会遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)    for(let key in obj) {        // obj.hasOwnProperty() 方法只考虑对象自身的属性        if(obj.hasOwnProperty(key)) {            newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key];        }    }    return newObj;}
复制代码


实现二:考虑 Symbol


// hash 作为一个检查器,避免对象深拷贝中出现环引用,导致爆栈function deepClone(obj, hash = new WeakMap()) {    if(!isObject(obj)) return obj;    // 检查是有存在相同的对象在之前拷贝过,有则返回之前拷贝后存于hash中的对象    if(hash.has(obj)) return hash.get(obj);    let newObj = Array.isArray(obj) ? [] : {};    // 备份存在hash中,newObj目前是空对象、数组。后面会对属性进行追加,这里存的值是对象的栈    hash.set(obj, newObj);    // Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。    Reflect.ownKeys(obj).forEach(key => {        // 属性值如果是对象,则进行递归深拷贝,否则直接拷贝        newObj[key] = isObject(obj[key]) ? deepClone(obj[key], hash) : obj[key];    });    return newObj;}
复制代码


----问题知识点分割线----

对 sticky 定位的理解

sticky 英文字面意思是粘贴,所以可以把它称之为粘性定位。语法:position: sticky; 基于用户的滚动位置来定位。


粘性定位的元素是依赖于用户的滚动,在 position:relativeposition:fixed 定位之间切换。它的行为就像 position:relative; 而当页面滚动超出目标区域时,它的表现就像 position:fixed;,它会固定在目标位置。元素定位表现为在跨越特定阈值前为相对定位,之后为固定定位。这个特定阈值指的是 top, right, bottom 或 left 之一,换言之,指定 top, right, bottom 或 left 四个阈值其中之一,才可使粘性定位生效。否则其行为与相对定位相同。


----问题知识点分割线----

继承

原型继承

核心思想:子类的原型成为父类的实例


实现


function SuperType() {    this.colors = ['red', 'green'];}function SubType() {}// 原型继承关键: 子类的原型成为父类的实例SubType.prototype = new SuperType();
// 测试let instance1 = new SubType();instance1.colors.push('blue');
let instance2 = new SubType();console.log(instance2.colors); // ['red', 'green', 'blue']
复制代码


原型继承存在的问题


  1. 原型中包含的引用类型属性将被所有实例对象共享

  2. 子类在实例化时不能给父类构造函数传参

构造函数继承

核心思想:在子类构造函数中调用父类构造函数


实现


function SuperType(name) {    this.name = name;    this.colors = ['red', 'green'];    this.getName = function() {        return this.name;    }}function SubType(name) {    // 继承 SuperType 并传参    SuperType.call(this, name);}
// 测试let instance1 = new SubType('instance1');instance1.colors.push('blue');console.log(instance1.colors); // ['red','green','blue']
let instance2 = new SubType('instance2');console.log(instance2.colors); // ['red', 'green']
复制代码


构造函数继承的出现是为了解决了原型继承的引用值共享问题。优点是可以在子类构造函数中向父类构造函数传参。它存在的问题是:1)由于方法必须在构造函数中定义,因此方法不能重用。2)子类也不能访问父类原型上定义的方法。

组合继承

核心思想:综合了原型链和构造函数,即,使用原型链继承原型上的方法,而通过构造函数继承实例属性。


实现


function SuperType(name) {    this.name = name;    this.colors = ['red', 'green'];}Super.prototype.sayName = function() {    console.log(this.name);}function SubType(name, age) {    // 继承属性    SuperType.call(this, name);    // 实例属性    this.age = age;}// 继承方法SubType.prototype = new SuperType();
// 测试let instance1 = new SubType('instance1', 1);instance1.sayName(); // "instance1"instance1.colors.push("blue");console.log(instance1.colors); // ['red','green','blue']
let instance2 = new SubType('instance2', 2);instance2.sayName(); // "instance2"console.log(instance2.colors); // ['red','green']
复制代码


组合继承存在的问题是:父类构造函数始终会被调用两次:一次是在创建子类原型时new SuperType()调用,另一次是在子类构造函数中SuperType.call()调用。

寄生式组合继承(最佳)

核心思想:通过构造函数继承属性,但使用混合式原型继承方法,即,不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。


实现


function SuperType(name) {    this.name = name;    this.colors = ['red', 'green'];}Super.prototype.sayName = function() {    console.log(this.name);}function SubType(name, age) {    // 继承属性    SuperType.call(this, name);    this.age = age;}// 继承方法SubType.prototype = Object.create(SuperType.prototype);// 重写原型导致默认 constructor 丢失,手动将 constructor 指回 SubTypeSubType.prototype.constructor = SubType;
复制代码

class 实现继承(ES6)

核心思想:通过 extends 来实现类的继承(相当于 ES5 的原型继承)。通过 super 调用父类的构造方法 (相当于 ES5 的构造函数继承)。


实现


class SuperType {    constructor(name) {        this.name = name;    }    sayName() {        console.log(this.name);    }}class SubType extends SuperType {    constructor(name, age) {        super(name);  // 继承属性        this.age = age;    }}
// 测试let instance = new SubType('instance', 0);instance.sayName(); // "instance"
复制代码


虽然类继承使用的是新语法,但背后依旧使用的是原型链。


----问题知识点分割线----

为什么有时候⽤translate 来改变位置⽽不是定位?

translate 是 transform 属性的⼀个值。改变 transform 或 opacity 不会触发浏览器重新布局(reflow)或重绘(repaint),只会触发复合(compositions)。⽽改变绝对定位会触发重新布局,进⽽触发重绘和复合。transform 使浏览器为元素创建⼀个 GPU 图层,但改变绝对定位会使⽤到 CPU。 因此 translate()更⾼效,可以缩短平滑动画的绘制时间。 ⽽translate 改变位置时,元素依然会占据其原始空间,绝对定位就不会发⽣这种情况。


----问题知识点分割线----

New 操作符做了什么事情?

1、首先创建了一个新对象2、设置原型,将对象的原型设置为函数的prototype对象3、让函数的this指向这个对象,执行构造函数的代码(为这个新对象添加属性)4、判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象
复制代码


----问题知识点分割线----

对浏览器的理解

浏览器的主要功能是将用户选择的 web 资源呈现出来,它需要从服务器请求资源,并将其显示在浏览器窗口中,资源的格式通常是 HTML,也包括 PDF、image 及其他格式。用户用 URI(Uniform Resource Identifier 统一资源标识符)来指定所请求资源的位置。


HTML 和 CSS 规范中规定了浏览器解释 html 文档的方式,由 W3C 组织对这些规范进行维护,W3C 是负责制定 web 标准的组织。但是浏览器厂商纷纷开发自己的扩展,对规范的遵循并不完善,这为 web 开发者带来了严重的兼容性问题。


浏览器可以分为两部分,shell 和 内核。其中 shell 的种类相对比较多,内核则比较少。也有一些浏览器并不区分外壳和内核。从 Mozilla 将 Gecko 独立出来后,才有了外壳和内核的明确划分。


  • shell 是指浏览器的外壳:例如菜单,工具栏等。主要是提供给用户界面操作,参数设置等等。它是调用内核来实现各种功能的。

  • 内核是浏览器的核心。内核是基于标记语言显示内容的程序或模块。


----问题知识点分割线----

如何提取高度嵌套的对象里的指定属性?

有时会遇到一些嵌套程度非常深的对象:


const school = {   classes: {      stu: {         name: 'Bob',         age: 24,      }   }}
复制代码


像此处的 name 这个变量,嵌套了四层,此时如果仍然尝试老方法来提取它:


const { name } = school
复制代码


显然是不奏效的,因为 school 这个对象本身是没有 name 这个属性的,name 位于 school 对象的“儿子的儿子”对象里面。要想把 name 提取出来,一种比较笨的方法是逐层解构:


const { classes } = schoolconst { stu } = classesconst { name } = stuname // 'Bob'
复制代码


但是还有一种更标准的做法,可以用一行代码来解决这个问题:


const { classes: { stu: { name } }} = school
console.log(name) // 'Bob'
复制代码


可以在解构出来的变量名右侧,通过冒号+{目标属性名}这种形式,进一步解构它,一直解构到拿到目标数据为止。


----问题知识点分割线----

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=>{})
复制代码


----问题知识点分割线----

实现 JSONP 跨域

JSONP 核心原理script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求;


实现


const jsonp = (url, params, callbackName) => {    const generateUrl = () => {        let dataSrc = "";        for(let key in params) {            if(params.hasOwnProperty(key)) {                dataSrc += `${key}=${params[key]}&`            }        }        dataSrc += `callback=${callbackName}`;        return `${url}?${dataSrc}`;    }    return new Promise((resolve, reject) => {        const scriptEle = document.createElement('script');        scriptEle.src = generateUrl();        document.body.appendChild(scriptEle);        window[callbackName] = data => {            resolve(data);            document.removeChild(scriptEle);        }    });}
复制代码


----问题知识点分割线----

对事件委托的理解

(1)事件委托的概念

事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件委托(事件代理)。


使用事件委托可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理还可以实现事件的动态绑定,比如说新增了一个子节点,并不需要单独地为它添加一个监听事件,它绑定的事件会交给父元素中的监听函数来处理。

(2)事件委托的特点

  • 减少内存消耗


如果有一个列表,列表之中有大量的列表项,需要在点击列表项的时候响应一个事件:


<ul id="list">  <li>item 1</li>  <li>item 2</li>  <li>item 3</li>  ......  <li>item n</li></ul>
复制代码


如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。因此,比较好的方法就是把这个点击事件绑定到他的父层,也就是 ul 上,然后在执行事件时再去匹配判断目标元素,所以事件委托可以减少大量的内存消耗,节约效率。


  • 动态绑定事件


给上述的例子中每个列表项都绑定事件,在很多时候,需要通过 AJAX 或者用户操作动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件;如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的,所以使用事件在动态绑定事件的情况下是可以减少很多重复工作的。


// 来实现把 #list 下的 li 元素的事件代理委托到它的父层元素也就是 #list 上:// 给父层元素绑定事件document.getElementById('list').addEventListener('click', function (e) {  // 兼容性处理  var event = e || window.event;  var target = event.target || event.srcElement;  // 判断是否匹配目标元素  if (target.nodeName.toLocaleLowerCase === 'li') {    console.log('the content is: ', target.innerHTML);  }});
复制代码


在上述代码中, target 元素则是在 #list 元素之下具体被点击的元素,然后通过判断 target 的一些属性(比如:nodeName,id 等等)可以更精确地匹配到某一类 #list li 元素之上;

(3)局限性

当然,事件委托也是有局限的。比如 focus、blur 之类的事件没有事件冒泡机制,所以无法实现事件委托;mousemove、mouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的。


当然事件委托不是只有优点,它也是有缺点的,事件委托会影响页面性能,主要影响因素有:


  • 元素中,绑定事件委托的次数;

  • 点击的最底层元素,到绑定事件元素之间的DOM层数;


在必须使用事件委托的地方,可以进行如下的处理:


  • 只在必须的地方,使用事件委托,比如:ajax的局部刷新区域

  • 尽量的减少绑定的层级,不在body元素上,进行绑定

  • 减少绑定的次数,如果可以,那么把多个事件的绑定,合并到一次事件委托中去,由这个事件委托的回调,来进行分发。


----问题知识点分割线----

渲染过程中遇到 JS 文件如何处理?

JavaScript 的加载、解析与执行会阻塞文档的解析,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停文档的解析,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。也就是说,如果想要首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。


----问题知识点分割线----

link 和 @import 的区别

两者都是外部引用 CSS 的方式,它们的区别如下:


  • link 是 XHTML 标签,除了加载 CSS 外,还可以定义 RSS 等其他事务;@import 属于 CSS 范畴,只能加载 CSS。

  • link 引用 CSS 时,在页面载入时同时加载;@import 需要页面网页完全载入以后加载。

  • link 是 XHTML 标签,无兼容问题;@import 是在 CSS2.1 提出的,低版本的浏览器不支持。

  • link 支持使用 Javascript 控制 DOM 去改变样式;而 @import 不支持。


----问题知识点分割线----

如何⽤webpack 来优化前端性能?

⽤webpack 优化前端性能是指优化 webpack 的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。


  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩 css

  • 利⽤CDN 加速: 在构建过程中,将引⽤的静态资源路径修改为 CDN 上对应的路径。可以利⽤webpack 对于 output 参数和各 loader 的 publicPath 参数来修改资源路径

  • Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动 webpack 时追加参数 --optimize-minimize 来实现

  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存

  • 提取公共第三⽅库: SplitChunksPlugin 插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

用户头像

还未添加个人签名 2022.09.05 加入

还未添加个人简介

评论

发布
暂无评论
字节前端必会面试题_JavaScript_夏天的味道123_InfoQ写作社区