2024前端面试高频题(一) JS篇

本文详细解释了JavaScript中this的关键特性,包括在不同调用模式下的行为(如函数调用、方法调用、构造器调用、apply/call/bind),箭头函数的独特性,以及ES6中的新特性如Set、Map、Proxy和模块化。作者还讨论了事件循环、异步与await以及原型和原型链的概念。

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

this 以及 this的指向

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。

  • 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。

    • 匿名函数自调和回调函数里的 this 指向 window。 严格模式(usestrict)下,this->undefined因为这类函数调用时,前边即没有.,也没有new!```js "use strict"; (function () { console.log(this) })(); var arr = [1] arr.forEach(function () { console.log(this) });

      
      
    • 箭头函数里的this,指向当前函数之外最近的作用域中的this。

  • 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。

     const obj = {
       name: 'lumi',
       fn: function() { console.log(this.name) }
     }
     obj.fn()
     
    
    
    
  • 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。

      function obj(name) {
        this.name = name;
        this.fn = function() {
          console.log(this.name)
        }
      }
      ​
      const person = new obj('lumi')
      person.fn(); 
      
    
    
    
  • 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。

call、bind、apply

作用:改变函数执行时的指向,改变this的指向

  
  function fn(..args) {
    console.log(this, 'this')
    console.log(args, 'args')
  }
  const obj = {
    name: 'lumi'
  }

call()、apply()

aplly()接收两个参数,第一个参数为 this 的指向,第二个参数为参数列表。

call() 第一个参数为this指向,后面传入的是一个参数列表。

只是暂时改变this指向。当第一个参数为nullundefined的时候,默认指向window。

    fn.call(obj,1,2)
    fn.apply(obj, [1,2])
    
    
    
    fn(1,2)
    
    
    

bind()

bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)

改变this指向后不会立即执行,而是返回一个永久改变this指向的函数(生成一个新的函数)

  const bindFn = fn.bind(obj)
  bindFn(1,2)
  

ES6 常见

箭头函数

特点:

  • 不能作为构造函数,无法使用 new

  • 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象

  • 不能使用 argument 对象

  • 单命令行时可以不写 return; 返回对象时,需要用括号包裹。

    var sum = (num1, num2) => { return num1 + num2; }
    
    var sum = (num1, num2) => num1 + num2;
    ​
    let getTempItem = id => ({ id: id, name: "Temp" });
    
    let getTempItem = id => {  return {id: id, name: "Temp"} };

数据类型
  • 基本类型(栈存储):String、Number、Boolean、Undefind、Null、Symbol

  • 引用类型(堆存储):Object、Array、Function

数据类型判断
  • typeof:其中数组、对象、null都会被判断为object,其他判断都正确。

  • instanceof :只能正确判断引用类型数据类型

  • constructor:判断数据类型;对象实例通过其访问它的构造函数

  • Object.prototype.toString.call(): 使用 Object 对象的原型方法 toString 来判断数据类型

判断数组
  • Object.prototype.toString.call()

  • 通过原型链:obj.__proto__ === Array.prototype

  • Array.isArray()

  • instanceof

  • Array.prototype.isPrototypeOf

rest 运算符(剩余运算符)

用于解构数组和对象,扩展运算符(...)被用在函数形参上时,它可以把一个分离的参数序列整合成一个数组

经常用于获取函数的多余参数,或者像上面这样处理函数参数个数不确定的情况。

总结

扩展运算符和rest运算符是逆运算

  • 扩展运算符:数组=>分割序列

  • rest运算符:分割序列=>数组

Set
  • 类似于数组,但成员值唯一。

  • 会比较类型, 5 不等于 ’5‘

  • null、undefined、NaN不会被过滤

  • 本身是一个构造函数,使用new实例化

使用场景:数组/字符串去重

属性方法:add、delete、has、clear、size

遍历:forEach遍历

    
    [...new Set([2, 3, 5, 4, 5, 2, 2])] 
    [...new Set('ababbc')].join('') 
    
    let s = new Set()
    s.add(1).add(2).add(2) 
    s.delete(2) 
    s.has(1) 
    s.clear() 
    s.size 

Map

类似于对象,也是键值对的集合,但属性不限于字符串,提供了“值—值”的对应,是一种更完善的 Hash 结构实现。只有引用地址一样,map结构才能视为同一个键。

与Object的区别

  • Object键只能是字符串/Symbol,但Map键可以是任意值

  • Map键值是有序的,对象则不是

  • Map 的键值对个数可以从 size 属性获取,而 Object 的键值对个数只能手动计算(通过keys数组个数)。

  • Object 都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。

属性方法:set、get、delete、has、clear、size、keys、values

    const m = new Map();
    const o = {p: 'Hello World'};
    ​
    m.set(o, 'content')
    m.get(o) 
    ​
    m.has(o) 
    m.delete(o) 
    m.has(o) 
    m.size 

遍历:通过 forEach 和 for…of,获取key与对应的value

    
    map.forEach(function (value, key) {
        console.log(key, value);
    });
    ​
    for (let o of map) {
        console.log(o) 
    }

Reflect

是一个内置对象,提供了一系列用于操作对象的方法。Reflect 将 Object 对象的一些明显属于语言内部的方法(in、delete),放到Reflect对象上(Reflect.get、Reflect.set)。

WeakMap

EventLoop 事件循环

众所周知,JS是 单线程,有且只有一个调用栈,先执行同步任务,再执行异步任务。

宏任务
  • 回调函数

  • script内容

  • setTimeout 和 setInterval、setImmediate

  • requestAnimationFrame

  • i/o操作

  • ui rendering 渲染

微任务

一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。

  • Promise.then

  • MutaionObserver

  • Object.observe(已废弃;Proxy 对象替代)

  • process.nextTick(Node.js)

async 与 await

async 是异步的意思,await则可以理解为 async wait。所以可以理解async就是用来声明一个异步方法,而 await是用来等待异步方法执行。

  • async :用来声明一个异步方法,返回一个promise 对象。

  • await:用来等待异步方法执行。await命令后面是一个 Promise对象,返回该对象的结果。如果不是 Promise对象,就直接返回对应的值。不管await后面跟着的是什么,await都会阻塞后面的代码

执行机制
  • 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中

  • 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完

举例说明
例子🌰1
    console.log('script start' ) 
    ​
    setTimeout(()=>{
        console.log('setTimeout')
    }, 0) 
    ​
    new Promise((resolve, reject)=>{
        console.log('new Promise') 
        resolve()
    }).then(()=>{
        console.log('promise then') 
    })
    ​
    console.log('script end') 

分析:

  1. 遇到console ,直接打印 script start

  2. 遇到定时器setTimeout,属于新的宏任务,留着

  3. 遇到promise,直接打印 new Promise

  4. 接着是promise.then,属于微任务,留着

  5. 遇到 console,直接打印 script end

  6. 第一轮宏任务(即主线程)执行结束,查看微任务列表,发现promise.then 回调,执行打印 promise end

  7. 当所有微任务执行结束后,执行下一个宏任务setTimeout,打印 setTimeout

例子🌰2
    async function async1() {
        console.log('async1 start') 
        await async2() 
        console.log('async1 end') 
    } 
    async function async2() {
        console.log('async2')
    }
    ​
    console.log('script start')  
    ​
    setTimeout(function () {
        console.log('settimeout')
    }) 
    ​
    async1() 
    ​
    new Promise(function (resolve) {
        console.log('promise1') 
        resolve() 
    }).then(function () {
        console.log('promise2') 
    })
    console.log('script end') 

分析:

  1. 遇到 console,执行打印 script start

  2. 遇到定时器,属于新的宏任务,留着

  3. 遇到 async1,执行,打印 async1 start,遇到 async2 , 执行打印 async2 , await会阻塞后面的代码(即加入微任务列表),跳出去执行同步任务

  4. 遇到 promise,执行打印 promise1

  5. 遇到promise.then,属于微任务,留着

  6. 遇到console,执行打印 script end

  7. 第一轮宏任务执行结束,检查微任务列表,先执行await后的代码,即打印 async1 end , 接着执行then,打印promise 2

  8. 微任务执行结束,执行下一个宏任务定时器,打印 setTimeout

类似Map,但键名只能是对象,键名所指向的对象,不计入垃圾回收机制。

EventFlow 事件流

存在三个阶段:事件捕获、目标阶段、事件冒泡。Dom标准事件流的触发的先后顺序为:先捕获再冒泡。即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。

事件冒泡

当一个元素接收到事件的时候,会把他接收到的事件传给自己的父级,一直到 window/dom。(该事件仅指click、dbclick等事件,而非绑定的函数方法)

例如:A元素包含B元素,点击B元素的同时就会冒泡触发A元素的点击事件。

事件捕获

当鼠标点击或者触发dom事件时(被触发dom事件的这个元素被叫作事件源),浏览器会从根节点 =>事件源(由外到内)进行事件传播。

与冒泡不同是,事件的传播方向,捕获是由外到内的,冒泡是由内到外的。

事件委托

又称事件代理。即利用事件冒泡,将子元素事件绑定到父元素上,如果子元素阻止了冒泡,则委托无法实现。

优点:

  • 替代循环绑定事件的操作,减少内存消耗,提高性能。

  • 简化dom节点更新时相应事件的更新。

缺点:

  • 事件委托基于冒泡,对于不冒泡的事件不支持。

  • 层级过多,冒泡过程中,可能会被某层阻止掉。

  • 理论上委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,比如在table上代理td,而不是在document上代理td

原型

了解原型需要先了解构造函数。由于Js 没有类的概念(es6有class关键词,使用方法后续拓展),所以使用 构造函数来实现继承机制。

构造函数

Js通过构造函数生成实例。但产生了一个问题:无法共享公共属性,构造函数中通过 this赋值的属性方法是每个实例独有的。

所以原型对象就是用来存储共享属性和方法的。

    
    
    function Student(name, age) {
      this.name = name
      this.age = age
    }
    
    const student1 = new Student('lumi', 18)

原型对象

每个函数在生成的时候,都会创建一个属性 prototype , 这个属性指向一个对象,即 原型对象

原型对象中有一个属性 constructor, 指向该函数,这样两者就联系起来了。

原型链

原型链就是实例对象和原型对象之间的联系。每个构造函数创建的每一个实例,都有一个属性 __proto__ ,这个属性指向构造函数的原型对象。通过该属性可以一步一步向上查找形成一个链式结构,称为 原型链

如果通过实例对象的 __proto__ 属性赋值 ,会改变其构造函数的原型对象,从而被所有该构造函数创建的实例所共享。

function Student(name, age) {
  this.name = name;
  this.age = age;
}
​

Student.prototype.school = '北京大学'
Student.prototype.goToClass = function() { console.log(`${this.name}上课了`) }
​

const s1 = new Student('lumi', 13);
const s2 = new Student('lucy', 15);
s1.goToClass(); 
s2.goToClass(); 
s2.hasOwnProperty('goToClass') 
s1.__proto__.leave = function() {
  console.log(`${this.name}放学了`)
} 
s2.leave() 
​

注意⚠️

生产环境中,不建议使用 __proto__,避免环境产生依赖。可以使用 Object.getPrototypeOf 方法来获取实例对象原型,然后再为原型添加方法和属性。

补充:原型链的尽头是null,详情见:

Proxy

Proxy代理:在目标对象之前假设一层拦截,可以对外界的访问进行改写。ES6提供原生的Proxy构造函数。

var proxy = new Proxy(target, handler)



常见拦截行为
  • get捕获器:用于拦截对象属性读取

  • set捕获器:用于拦截对象属性赋值

  • has捕获器:用于拦截判断target对象中是否含有某个属性

  • deleteProperty捕获器:用于拦截删除target对象属性的操作

    
    let person = {
      name: 'lumi'
    }
    var proxy = new Proxy(person, {
      get: function(target, key) {
        if (key in target) {
          return target[key];
        }else{
          return 'not found'
        }
      },
      set: function(target, key, value) {
        target[key] = value
        return true
      },
      has: (target, key) => {
        return key in target
      },
      deleteProperty: (target, key) => {
        if (key === 'name') {
          throw new Error('name不可被删除')
        } else {
          return delete target[key]
        }
      }
    })
    console.log(proxy.name) 
    console.log(proxy.age) 
    proxy.age = 18
    console.log(proxy.age) 
    console.log('age' in proxy) 
    console.log('sex' in proxy) 
     
    delete proxy.age
    console.log('age' in proxy) 

模块化

将js分割成不同职责的js,⽤于解决全局变量污染、变量冲突、代码冗余等问题,提高代码可维护性、可拓展性、复用性。

自执行函数实现模块化

通过函数作用域解决了命名冲突、污染全局作用域的问题。

    
    (function () {
        var a = 1;
        console.log(a); 
    })();
    ​
    (function () {
        var a = 2;
        console.log(a); 
    })();

AMD

异步模块定义,采用异步方法加载模块,模块加载不影响后面语句运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到所有依赖项都加载完成之后,这个回调函数才会运行。【前置依赖】

CMD

公共模块定义,可以使用 require 同步加载依赖,也可以使用 require.async 来异步加载依赖。【后置依赖】

CommonJs

Node的⼀种模块同步加载规范,⼀个⽂件即是⼀个模块。用在Node端(服务端),加载速度很快,所以可以使用同步方法。

  • 使⽤时require引⼊。

  • module.exports 是 CommonJS 的⼀个 exports 变量,提供对外的接⼝。

  • 输出为一个值的拷贝。

ESModule

是ES6提供的官方js模块化方案。目前浏览器还不能全面支持 ESModule 的语法,需要用 babel 进行解析。

通过export命令显式指定输出的代码。属于编译时加载,⽐Commonjs效率⾼。可以按需加载指定⽅法。效率⾼。

  • export defalut 与 export 是 ES6 Module 中对外暴露数据的。 export defalut 是向外部默认暴露数据,使⽤时 import 引⼊时需要为该模块指定⼀个任意名称,import 后不可以使⽤{};

  • export 是单个暴露数据或⽅法,利⽤ import{}来引⼊,并且{}内名称与 export ⼀⼀对应,{}也可以使⽤ as 为某个数据或⽅法指定新的名称;

  • 输出为一个值的引用。

作者-麦浪冒险家,欢迎关注! 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Web面试那些事儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值