前端JavaScript面试题(2)

发布于:2025-06-14 ⋅ 阅读:(21) ⋅ 点赞:(0)

✨✨✨目录 

1.箭头函数与普通函数的区别?

2.箭头函数的this指向哪里?

3.扩展运算符的作用及使用场景?

4.对对象\数组解构的理解?

5.你是怎么理解ES6中Proxy的?使用场景有哪些?

6.说说对 ES6 中rest参数的理解?

7.Map和Object的区别?

8.Map和WeakMap的区别?

9.JavaScript有哪些内置对象?

10.正则表达式运用及使用场景?

11.JavaScript脚本延迟加载的方式有哪些?

12.什么是类数组对象?如何转化为数组?

13.为什么函数的arguments参数是类数组而不是数组?如何遍历类数组?

14.数组有哪些原生方法?

15.Unicode、UTF-8、UTF-16、UTF-32的区别?


1.箭头函数与普通函数的区别?

(1)箭头函数比普通函数更加简洁

箭头函数省去了function关键字,采用箭头=>来定义函数。函数的参数放在=>前面的括号中,函数体跟在=>后的花括号中。

(2)箭头函数没有自己的this

箭头函数不会创建自己的this,所以它没有自己的this,它只会从自己的作用域链的上一层继承this。所以,箭头函数中this的指向在它被定义的时候就已经确定了,之后永远不会改变。

(3)箭头函数继承来的this指向永远不会改变

对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。

(4).call()/.apply()/.bind()无法改变箭头函数中this的指向

.call()/.apply()/.bind()方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向,虽然这么做代码不会报错。

(5)箭头函数不能作为构造函数使用

我们先了解一下构造函数的new都做了些什么?简单来说,分为四步:

① JS内部首先会先生成一个对象; ② 再把函数中的this指向该对象; ③ 然后执行构造函数中的语句; ④ 最终返回该对象实例。

但是因为箭头函数没有自己的this,它的this其实是继承了外层执行环境中的this,且this指向永远不会随在哪里调用、被谁调用而改变,所以箭头函数不能作为构造函数使用,或者说构造函数不能定义成箭头函数,否则用new调用时会报错!

(6)箭头函数没有自己的arguments

箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。

(7)箭头函数没有prototype

let sayHi = () => {
    console.log('Hello World !')
};
console.log(sayHi.prototype); // undefined

(8)箭头函数不能用作Generator函数,不能使用yeild关键字

2.箭头函数的this指向哪里?

箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于自己的this,它所谓的this是捕获其所在上下⽂的 this 值,作为自己的 this 值。由于没有属于自己的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。

可以⽤Babel理解⼀下箭头函数:

// ES6 
const obj = { 
  getArrow() { 
    return () => { 
      console.log(this === obj); 
    }; 
  } 
}

转化后:

// ES5,由 Babel 转译
var obj = { 
   getArrow: function getArrow() { 
     var _this = this; 
     return function () { 
        console.log(_this === obj); 
     }; 
   } 
};

3.扩展运算符的作用及使用场景?

‌扩展运算符(...)是JavaScript和TypeScript中的核心语法,主要用于展开数组、对象或可迭代对象的元素或属性,适用于函数参数传递、数组/对象操作、解构赋值等场景。

(1)对象扩展运算符

浅拷贝与合并‌:
对象复制:const obj2 = {...obj1}等同于Object.assign({}, obj1)‌‌
合并属性(后者覆盖前者):{...obj1, ...obj2}‌‌
动态修改属性:{...state, count: newValue}(常用于Redux更新状态)。‌‌

‌可枚举属性限制‌:
仅复制对象自身的可枚举属性,原型链属性不包含。‌‌

(2)数组扩展运算符

‌复制与合并‌:
创建数组副本:const arr2 = [...arr1]‌‌
合并多个数组:const merged = [...arr1, ...arr2]‌‌

参数序列转换‌:
将数组展开为函数参数:Math.max(...)替代apply语法。‌‌
嵌套数组单层展开:console.log(...[1,2,3]) // 1 2 3;console.log(...[1,[2,3,4],5]) // 1 [2,3,4] 5

解构赋值应用‌:
提取剩余元素:const [a, ...rest] = (rest为``)。‌‌‌‌
仅允许在解构末尾使用:[...rest, a]会报错。‌‌

将字符串转为真正的数组:[...'hello'] // ['h','e','l','l','o']

4.对对象\数组解构的理解?

对象和数组解构是ES6提供的一种新特性,允许通过模式匹配的方式从对象或数组中提取值,并将其赋给变量。‌

对象解构严格以属性名来提取对应的值,并将其赋给变量。对于高度嵌套的对象,可以采用冒号+{目标属性名}进一步解构,例如:

const person = { name: 'Alice', age: 25, gender: 'female' };
const { name, age } = person;
console.log(name); // 输出: Alice
console.log(age); // 输出: 25
const school = {
  classes: {
    stu: {
      name: 'Alice',
      age: '25'
    }
  }
}
const {classes:{ stu:{name} }} = school;
console.log(name) // Alice

数组解构根据数组的索引位置来提取对应的值,并将其赋给变量。例如:

const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;
console.log(first); // 输出: 1
console.log(second); // 输出: 2
console.log(rest); // 输出: [3, 4, 5]

5.你是怎么理解ES6中Proxy的?使用场景有哪些?

 ES6中的Proxy ‌是一种用于创建对象代理的机制,它允许开发者定义额外的行为来拦截和改写对目标对象的常规操作。Proxy通过定义一系列的“陷阱”(traps),如get、set、apply等,来拦截和自定义对象的基本操作。这些操作包括属性读取、赋值、函数调用等,从而实现对目标对象的控制和扩展‌。

Proxy的基本语法和作用
Proxy的基本语法如下:let proxy = new Proxy(target, handler);

  • target‌:被代理的目标对象,可以是任何类型的对象。
  • ‌handler‌:一个包含各种陷阱的处理器对象,用于定义代理的行为。这些陷阱包括getsetapplyhasdeleteProperty等‌。

Proxy的使用场景

(1)数据验证‌:在设置对象属性时进行数据有效性验证,例如限制数值范围或格式校验。通过set陷阱可以自定义验证逻辑,不符合条件的赋值操作会被拒绝‌。

(2)属性拦截‌:拦截并自定义对象的属性读取和赋值操作。例如,可以在读取属性时添加日志记录,或在设置属性时进行权限检查‌。

(3)函数调用‌:通过apply陷阱拦截函数调用,可以在函数执行前后添加自定义逻辑,如权限检查或性能监控‌。

(4)对象监控‌:通过getset陷阱监控对象的访问和修改,适用于需要跟踪对象状态变化的场景‌。

(5)动态对象属性‌:在运行时动态定义和删除对象的属性,通过definePropertydeleteProperty陷阱实现‌。

示例:使用 Proxy 拦截对象属性的读取和设置

// 定义一个目标对象
const target = {
  name: 'Alice',
  age: 25
};

// 定义一个处理器对象,用于拦截目标对象的操作
const handler = {
  // 拦截读取操作
  get(target, prop) {
    console.log(`读取属性: ${prop}`);
    if (prop in target) {
      return target[prop];
    } else {
      throw new Error(`属性 ${prop} 不存在`);
    }
  },
  // 拦截设置操作
  set(target, prop, value) {
    console.log(`设置属性: ${prop} = ${value}`);
    if (prop in target) {
      target[prop] = value;
      return true; // 表示设置成功
    } else {
      throw new Error(`属性 ${prop} 不存在`);
    }
  }
};

// 创建一个 Proxy 对象
const proxy = new Proxy(target, handler);

// 测试 Proxy 对象
console.log(proxy.name); // 读取属性: name,然后输出: Alice
proxy.age = 30; // 设置属性: age = 30
console.log(proxy.age); // 读取属性: age,然后输出: 30

// 尝试访问不存在的属性
try {
  console.log(proxy.gender); // 会抛出错误: 属性 gender 不存在
} catch (error) {
  console.error(error.message);
}

// 尝试设置不存在的属性
try {
  proxy.gender = 'female'; // 会抛出错误: 属性 gender 不存在
} catch (error) {
  console.error(error.message);
}

6.说说对 ES6 中rest参数的理解?

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

function add(...values) {
  let result = 1;
  for (var val of values) {
    result *= val;
  }
  return sum;
}
add(1, 2, 3, 4) // 24

下面是一个 rest 参数代替arguments变量的例子:

/ arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。下面是一个利用 rest 参数改写数组push方法的例子:

function push(array, ...items) {
  items.forEach(function(item) {
    array.push(item);
    console.log(item);
  });
}

var a = [];
push(a, 1, 2, 3)

注意:rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错;函数的length属性,不包括 rest 参数;箭头函数不可以使用arguments对象,该对象在函数体内不存在,如果要用,可以用 rest 参数代替。

(function(a) {}).length  // 1
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1

7.Map和Object的区别?

在JavaScript中,Map和Object是用于存储键值对数据的两种不同的数据结构(Map是ES6新增的数据结构)。它们在构造方式、键的类型以及原型继承等方面存在区别。

(1)构造方式

Map:只能通过构造函数new Map()创建

Object:可以通过字面量、new Object()、Object.create()等方式创建

(2)键类型支持

Map:Map的键可以是任意类型,包括对象、数组、函数等

Object:Object的键只能是字符串或者Symbol,其他类型的键会被转换为字符串

(3)键的顺序

Map:Map会保持键值对的插入顺序,所以是有序的

Object:对象是无序的,尽管ES6开始对象保留了字符串和Symbol类型key的创建顺序,但在存在数字key的情况下,会优先迭代数字key。通常按照数值、字符串、Symbol的顺序排列

(4)键值大小

Map:使用 .size 属性来获取其大小。使用 .get() 方法获取值。

Object: 没有内置的方法来获取其大小(键值对的数量),可采用Object.keys(xxx).length。使用 . 获取值。

(5)原型继承

Map:Map不会从原型链中继承属性。

Object:通过字面量创建的对象会继承来自Object.prototype的属性,例如toString、constructor等。

(6)迭代
Map:Map是可迭代的,实现了iterator接口,可以使用for...of或.forEach循环。使用 .keys()、.values()、.entries() 获取迭代器。

Object:对象本身不可直接迭代,但可以先遍历其键或值的集合,在迭代。通过for...in循环,再使用Object.keys()Object.values()、Object.entries等方法获取键、值或键值对。

(7)性能

Map:Map提供了有序性和灵活的键类型,在频繁添加和删除键值对的场景下进行了优化,性能表现更好。Map在处理大量数据时性能优于Object,尤其是当键值对数量非常大时。

Object:Object则更适合作为普通对象的容器,尤其在不需要严格顺序和键类型单一的场景中使用。在处理少量数据时,Object的性能优于Map,但在处理大量数据时性能较差。

    (8)JSON支持

    Map:JSON不支持Map格式。

    Object:对象可以直接被JSON处理,适用于与后端接口交互。

    (9)API对比

    操作 Map Object
    添加键值对 map.set(key,value) obj[key]=value
    获取值 map.get(key) obj[key]
    检查是否存在键 map.has(key) obj.hasOwnProperty(key)
    键值大小 map.size Object.keys(obj).length

    详细:JavaScript中Map与Object的区别_js map对象和object区别-CSDN博客

    8.Map和WeakMap的区别?

    上一题有说关于Map的内容。什么是WeakMap?WeakMap是ES6新增的一种集合类型,叫做’弱映射‘。它和Map是兄弟关系,与Map的区别在于这个弱字,API还是Map的API。

    (1)WeakMap的键必须为对象引用

    只接受对象作为键名(null除外),不接受其它类型的值作为键名。

    (2)WeakMap的键名引用的对象是弱引用

    强引用是指在代码中明确地持有对对象的引用,使对象不能被垃圾回收。只要存在强引用指向一个对象,该对象就会一直存在于内存中,垃圾回收器不会将其回收。JavaScript中的大多数引用都是强引用,例如通过变量、属性、闭包等方式持有的对象引用。

    弱引用是指即使存在对对象的引用,但垃圾回收器可以在任何时候回收该对象。弱引用通过 WeakReference 类实现,无论内存是否足够,只要发生垃圾回收,弱引用对象就会被回收。弱引用主要用于解决内存泄露问题,特别是在缓存场景中。

    两者区别:

    • Map的键可以是任意类型,WeakMap只接受对象作为键,不接受其它类型的值作为键
    • Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键;WeakMap的键是弱引用,键所指向的对象是可以被垃圾回收,此时键是无效的
    • Map可以被遍历,WeakMap不能被遍历。WeakMap的遍历操作(如.forEach())可能返回未被回收的对象,但无法保证这些对象后续状态的一致性
    • WeakMap不支持直接扩展或修改键集合的大小

    9.JavaScript有哪些内置对象?

    在JavaScript中,内置对象指的是那些预定义的对象,你可以直接在全局作用域中使用它们。它们提供了核心的编程功能,比如操作数组、处理日期时间、执行数学计算等。下面是JavaScript中一些常用的内置对象。
    (1)全局对象(Global Object)
    在浏览器中,全局对象是 window。在Node.js中,全局对象是 global

    (2)Object

    (3)Array
    用于处理数组。提供了许多有用的方法来操作数组,如 push()pop()shift()unshift()slice()splice() 等。

    (4)String
    用于处理字符串。提供了许多方法来操作字符串,如 charAt()indexOf()slice()split()substring() 等。

    (5)Number
    用于处理数字。提供了一些方法来处理数字,如 toFixed()toPrecision()parseInt()parseFloat() 等。

    (6)Boolean

    (7)Function
    用于创建新的函数。虽然这不是一个内置对象,但它是所有函数的基础。

    (8)Date
    用于处理日期和时间。提供了许多方法来获取和设置日期和时间,如 getDate()setDate()getFullYear()setFullYear() 等。

    (9)Math
    提供了一系列数学常数和函数,如 Math.PIMath.sqrt()Math.sin()Math.cos() 等。

    (10)RegExp
    用于处理正则表达式。虽然这不是一个内置对象,但它是正则表达式对象的构造函数。

    (11)JSON
    用于解析和序列化JSON数据。提供了 JSON.parse() 和 JSON.stringify() 方法。

    (12)Map和Set
    ES6中新增的数据结构,分别用于存储键值对集合和唯一值的集合。它们不是全局对象,但它们是内置的全局构造函数。

    (13)Promise和Symbol

    ES6中新增的用于处理异步操作和唯一标识符的构造函数。它们同样不是全局对象,但也是内置的全局构造函数。

    10.正则表达式运用及使用场景?

    正则表达式(Regular Expression,简称 regex 或 regexp)是一种用于匹配字符串中字符组合的模式。它通过定义一系列规则来描述字符串的特征,从而实现对文本的查找、替换、分割等操作。正则表达式功能强大且灵活,广泛应用于各种文本处理场景。

    构建正则表达式的两种方式:

    (1)字面量创建,其由包含在斜杠之间的模式组成

    const re = /\d+/g;

    (2)调用RegExp对象的构造函数

    const re = new RegExp("\\d+","g");
    
    const rul = "\\d+"
    const re1 = new RegExp(rul,"g");

    详细:JavaScript正则表达式解析:模式、方法与实战案例_js正则表达式解析-CSDN博客

    11.JavaScript脚本延迟加载的方式有哪些?

    延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。一般有以下几种方式:

    • defer 属性: 给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
    • async 属性: 给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
    • 动态创建 DOM 方式: 动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
    • 使用 setTimeout 延迟方法: 设置一个定时器来延迟加载js脚本文件。
    • 让 JS 最后加载: 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

    12.什么是类数组对象?如何转化为数组?

    一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。

    常见的类数组转换为数组的方法有这样几种:

    (1)通过 call 调用数组的 slice 方法来实现转换

    Array.prototype.slice.call(arrayLike);

    (2)通过 call 调用数组的 splice 方法来实现转换

    Array.prototype.splice.call(arrayLike, 0);

    (3)通过 apply 调用数组的 concat 方法来实现转换

    Array.prototype.concat.apply([], arrayLike);

    (4)通过 Array.from 方法来实现转换

    Array.from(arrayLike);

    (5)扩展运算符...

    const arr = [...arrayLike];

    13.为什么函数的arguments参数是类数组而不是数组?如何遍历类数组?

    arguments是一个对象,它的属性是从0开始依次递增的数字,还有length属性,但没有数组常见的方法属性,如forEach、reduce等,所以是类数组。

    通过Array.from、扩展运算符等方法将类数组转化为数组,再通过数组方法遍历。

    14.数组有哪些原生方法?

    push(): 在数组末尾添加一个或多个元素,并返回新数组的长度。

    pop(): 移除并返回数组末尾的元素。

    unshift(): 在数组开头添加一个或多个元素,并返回新数组的长度。

    shift(): 移除并返回数组开头的元素。

    concat(): 合并两个或更多数组,并返回新的合并后的数组,不会修改原始数组。

    slice(): 从数组中提取指定位置的元素,返回一个新的数组,不会修改原始数组。

    splice(): 从指定位置删除或替换元素,可修改原始数组。

    indexOf(): 查找指定元素在数组中的索引,如果不存在则返回-1。

    lastIndexOf(): 从数组末尾开始查找指定元素在数组中的索引,如果不存在则返回-1。

    includes(): 检查数组是否包含指定元素,返回一个布尔值。

    join(): 将数组中的所有元素转为字符串,并使用指定的分隔符连接它们。

    reverse(): 颠倒数组中元素的顺序,会修改原始数组。

    sort(): 对数组中的元素进行排序,默认按照字母顺序排序,会修改原始数组。

    filter(): 创建一个新数组,其中包含符合条件的所有元素。

    map(): 创建一个新数组,其中包含对原始数组中的每个元素进行操作后的结果。

    reduce(): 将数组中的元素进行累积操作,返回一个单一的值。

    forEach(): 对数组中的每个元素执行提供的函数。

    15.Unicode、UTF-8、UTF-16、UTF-32的区别?

    ASCII码称为美国标准信息交换码,它包含"A-Z"(包含大小写),数字"0-9"以及一些常见的符号。

    Unicode是一个统一的字符集,为全球所有语言的字符分配了唯一的代码点,可以说是ASCII的超集,又称统一码、万国码、单一码。例如,英文字母A的代码点是U+0041,中文汉字“中”的代码点是U+4E2D。Unicode通过不同的编码方式(如UTF-8、UTF-16、UTF-32)来实现字符的存储和传输‌。

    UTF-8是一种可变长度的编码方式,使用1到4个字节来表示一个字符。对于ASCII字符(0x00到0x7F),UTF-8使用1个字节;对于非ASCII字符,使用多字节表示。UTF-8的优点包括空间效率高、兼容性好,并且是互联网和现代文件格式的主流编码方式。然而,可变长度编码增加了处理的复杂性,并且对于非ASCII字符,存储和传输成本较高‌。

    UTF-16使用2个或4个字节来表示一个字符。基本多文种平面(BMP)内的字符使用2个字节,辅助平面内的字符使用4个字节(通过代理对实现)。UTF-16在处理BMP内的字符时效率较高,适合内存操作。然而,处理非BMP字符时占用空间较大‌。

    UTF-32是一种固定长度的编码方式,使用4个字节(32位)来表示每一个Unicode码点。无论字符是否在BMP内,UTF-32都使用相同数量的字节进行编码。

     💕💕💕持续更新中...... 

    若文章对你有帮助,点赞❤️、收藏⭐加关注➕吧!