前端JavaScript面试题(2)
✨✨✨目录
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:一个包含各种陷阱的处理器对象,用于定义代理的行为。这些陷阱包括
get
、set
、apply
、has
、deleteProperty
等。
Proxy的使用场景
(1)数据验证:在设置对象属性时进行数据有效性验证,例如限制数值范围或格式校验。通过set
陷阱可以自定义验证逻辑,不符合条件的赋值操作会被拒绝。
(2)属性拦截:拦截并自定义对象的属性读取和赋值操作。例如,可以在读取属性时添加日志记录,或在设置属性时进行权限检查。
(3)函数调用:通过apply
陷阱拦截函数调用,可以在函数执行前后添加自定义逻辑,如权限检查或性能监控。
(4)对象监控:通过get
和set
陷阱监控对象的访问和修改,适用于需要跟踪对象状态变化的场景。
(5)动态对象属性:在运行时动态定义和删除对象的属性,通过defineProperty
和deleteProperty
陷阱实现。
示例:使用 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.PI
, Math.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都使用相同数量的字节进行编码。
💕💕💕持续更新中......
若文章对你有帮助,点赞❤️、收藏⭐加关注➕吧!