金三银四跳槽季,前端面试题记录(2021),JS

本文深入讲解前端开发中的关键技术和概念,包括脚本标签中的defer与async的区别、DOM事件的特性、箭头函数的特点、变量声明的区别等,还涉及到了一些高级主题如深拷贝、柯里化函数的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Q、 script标签中defer和async的区别

defer: 中文意思是延迟。用途是表示脚本会被延迟到整个页面都解析完毕后再运行。因此,在script元素中设置defer属性,相当于告诉浏览器立即下载,但延迟执行。HTML5规范要求脚本按照它们出现的先后顺序执行,因此第一个延迟脚本会先于第二个延迟脚本执行,但执行脚本之间存在依赖,需要有执行的先后顺序时,就可以使用defer,延迟执行。我觉得把script脚本放在body底部和defer差不多。

defer流程:
1.浏览器开始解析HTML页面
2.遇到有defer属性的script标签,浏览器继续往下面解析页面,且会并行下载script标签的外部js文件
3.解析完HTML页面,再执行刚下载的js脚本(在DOMContentLoaded事件触发前执行,即刚刚解析完html,且可保证执行顺序就是他们在页面上的先后顺序

async: 中文意思是异步,这个属性与defer类似,都用于改变处理脚本的行为。同样与defer类似,async只适用于外部脚本文件,并告诉浏览器立即下载文件。但与defer不同的是,标记为async的脚本并不保证按照它们的先后顺序执行。
指定async属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容,这使用于之间互不依赖的各脚本。
async流程:
1.浏览器开始解析页面
2.遇到有sync属性的script标签,会继续往下解析,并且同时另开进程下载脚本
3.脚本下载完毕,浏览器停止解析,开始执行脚本,执行完毕后继续往下解析

注意:如script标签同时存在defer和async属性,则会当做async属性标签运行加载,defer将会无效;

Q、DOM各级的事件的特性

DOM 0级: 通过DOM直接赋值事件方法进行事件绑定,有事件冒泡阶段,但没有事件捕获阶段;

<button type="button" onclick="showFn()"></button>

<script>
	// 方法一: 直接写在标签
    function showFn() {
        alert('Hello World');
    }

	// 方法二: 获取dom然后赋值事件方法
	var btn = document.getElementById('btn');
	
    btn.onclick = function() {
        alert('Hello World');
    }

    // btn.onclick = null; 解绑事件
</script>

DOM 2级: 在DOM 0级基础上,增加了addEventListener、removeEventListener两个方法,支持一个dom元素单个事件能进行多个方法的绑定,还能选择是否在捕获阶段or冒泡阶段触发;

<button id="btn" type="button"></button>

<script>
    var btn = document.getElementById('btn');

    function showFn() {
        alert('Hello World');
    }

    btn.addEventListener('click', showFn, false);

    // btn.removeEventListener('click', showFn, false); 解绑事件 
</script>

DOM 3级: 支持了更多的事件类型,如触摸、滑动、滚轮、dom变动,并且能够然开发者自定义事件;

事件流执行顺序: 先捕获,然后处理,然后再冒泡出去。如下图,从左往右
在这里插入图片描述

Q、 箭头函数的特性

this指向:函数的this指向函数外部的this,定义的时候就已经确定好;
不能作为构造函数去使用;
没有arguments,如果在箭头函数中获取arguments,则会获取到函数外部的arguments值;
call、apply、bind无法影响箭头函数的this指向;
没有prototype原型;
无法使用yield;

Q、 var、const、let的区别

变量提升:var可以可以在声明前调用,会返回undefined;而使用const、let声明的变量(常量),则会有暂时性死区,在声明前调用会直接报错;
暂时性死区
多次声明:var 可以在同一作用域下多次声明,而使用const、let则不可以;
作用域:var不存在块级作用域,而const、let则存在块级作用域概念

Q、 基本类型和复杂类型

基本类型:Boolean Number String Undefined Null Symbol BigInt
复杂类型: object

可变性: 基本类型是不可通过原型方法改变值,只能通过重新赋值的方法改变其值;而复杂类型的值是可改变的,例如对象就可以通过修改对象属性值更改对象;

存储位置: 基本数据类型是存放在栈区的, 复杂类型是同时保存在栈区和堆区中的,栈区保存变量标识符和指向堆内存的地址

Q、 BigInt

js的数值类型遵循着保存成 64 位浮点数,所以只能保存正负9007199254740992的数字,所以ES22020推出一个新的数字类型BigInt用来作大整数的运算;只要在数字后面加小写n则代表这个是BigInt类型数字;
BigInt不能跟Number混合计算

console.log(190071992547409921231231); 	// Infinity

const bigintNumber = 9007199254740992123123123n;
const sum = bigintNumber  + 54545213;    // 报错

阮一峰ES6 BigInt

Q、 Iterator接口平时用过吗,用来实现了什么

阮一峰ES6 Iterator

Q、 ES5 的继承和 ES6 的继承区别

ES5 的继承时通过 prototype 或构造函数机制来实现。

  • ES5 的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.apply(this))。
  • ES6 的继承机制完全不同,实质上是先创建父类的实例对象 this(所以必须先调用父类的 super()方法),然后再用子类的构造函数修改
    this。

具体: ES6 通过 class 关键字定义类,里面有构造方法,类之间通过 extends 关键字实现继承。子类必须在 constructor 方法中调用 super 方法,否则新建实例报错。因为子类没有自己的 this 对象,而是继承了父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类得不到 this 对象。

注意:super 关键字指代父类的实例,即父类的 this 对象。在子类构造函数中,调用 super 后,才可使用 this 关键字,否则报错。

Q、 宏任务、微任务以及事件队列

「硬核JS」一次搞懂JS运行机制

Q、 localstorage、sessionstorage的区别

有效期: localstorage的有效期是永久,sessionstorage关闭浏览器则会删除掉
作用域: localstorage能在不同标签页共享,sessionstorage不可以

Q、 common JS和ES module

common JS:
Nodejs 环境所使用的模块系统就是基于CommonJs规范实现的,由于es6之前并没有一个模块化的正式规范,所以node自己实现了一个模块化的规范,使用module.exports导出,require引入

// a.js
module.exports = { age: 1, a: 'hello', foo:function(){} }

// b.js
const aJs = require(./a.js);

ES module:
ES6规范的模块化方案,使得引入的时候尽量静态化,在编译的时候确定是否使用,直接进行编译分析,使得可以进行tree shaking。还能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。

// a.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export { firstName, lastName, year };

// b.js
import { firstName, lastName, year } from './a.js';

差异:

  • CommonJs是单个值导出,ES6 Module可以导出多个
  • CommonJs导出的是变量的一份复制,一旦引入,其内部怎么变化都不会改变复制的这一份;ES6 Module导出的是变量的绑定,别的地方改变了值,所有引入的地方就会一起改变
  • CommonJs是动态导入语法可以写在判断里,ES6 Module静态语法编译时生成,只能写在顶层
  • CommonJs可以更改引入的值,ES6 Module是只读的,重新赋值时会报错

Q、 手写数组去重


    var list = [1, 2, 3, 4, 5, 1, 2, 3, 1, 1, 1123, 63, 1123];

    // 方法一:es6 set类型
    [...new Set(list)]

    // 方法二:indexOf判断
    function unique1(arr) {
        if (!Array.isArray(arr)) {
            console.log('type error!')
            return
        }
        let res = []
        for (let i = 0; i < arr.length; i++) {
            if (res.indexOf(arr[i]) === -1) {
                res.push(arr[i])
            }
        }
        return res
    }

    // 方法三: Obj赋值
    function unique2(arr) {
        if (!Array.isArray(arr)) {
            console.log('type error!')
            return
        }
        let res = [],
            obj = {}
        for (let i = 0; i < arr.length; i++) {
            if (!obj[arr[i]]) {
                res.push(arr[i])
                obj[arr[i]] = 1
            } else {
                obj[arr[i]]++
            }
        }
        return res
    }

Q、 手写深拷贝

主要注意点:遇到引用类型是递归里面的数据进行赋值,直到没有引用类型为止

function deepCopy( target , source ){       //source代表你想要被拷贝的元素 , target代表你想要拷贝的元素
    //1.for in 循环拿到 被拷贝元素 里面的属性
    for(let key in source){
        //2.拿到 被拷贝元素 里面的属性的值
        let sourceValue = source[key]
        //3.拿到遍历到的属性值的时候,需要判断这个属性值是不是引用类型
        if(sourceValue instanceof Object){
            //3.1如果是引用类型,做以下处理
            let subTarget = new sourceValue.constructor  
            /**
            *这里说一下:引用类型都会有一个constructor属性,数组的constructor会返回Array,对象的constructor会返回Object
            *所以new sourceValue.constructor 就省去了判断它是数组还是对象
            */
            target[key] = subTarget
            //递归
            deepCopy(subTarget,sourceValue)
        }else{
                //3.2如果不是引用类型,直接赋值就完事了
            target[key] = sourceValue
        }
    }
}

Q、 手写柯里化函数

function currying(fn) {
        var allArgs = [];

        // 运用闭包,将函数存于内存,这样就可以储存参数
        return function next() {
            var args = [...arguments];
            if (args.length > 0) {

                // 合并传入参数,储存参数
                allArgs = allArgs.concat(args);
                return next;
            } else {
                // 如果没有传入参数》(),则返回结果
                return fn.apply(null, allArgs);
            }
        }
    }
    var add = currying(function () {
        var sum = 0;
        for (var i = 0; i < arguments.length; i++) {
            sum += arguments[i];
        }
        return sum;
    });

    console.log(add(1)(2)(3, 4)(5)());   // 15

Q、 手写call

Function.prototype.myCall = function (context, ...arr) {
    if (context === null || context === undefined) {
       // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
        context = window 
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }
    const specialPrototype = Symbol('特殊属性Symbol') // 用于临时储存函数
    context[specialPrototype] = this; // 函数的this指向隐式绑定到context上
    let result = context[specialPrototype](...arr); // 通过隐式绑定执行函数并传递参数
    delete context[specialPrototype]; // 删除上下文对象的属性
    return result; // 返回函数执行结果
};

Q、 一次性插入1000个div,如何优化插入的性能;

使用Fragment

Q、 向1000个并排的div元素中,插入一个平级的div元素,如何优化插入的性能

  • 使用Fragment
  • 赋予key,然后使用virtual-dom,先render,然后diff,最后patch
  • 脱离文档流,用GPU去渲染,开启硬件加速

注意: 实际上浏览器有做优化,使用Fragment并不比innerHTML插入快多少,具体有待深究;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值