ECMA

面试
ECMA

数组

类数组转换为数组

Array.prototype.slice.call(arrayLike)

Array.from(arrayLike)

[...arrayLike]

[].concat(...arrayLike)

isArray

isArray 的前身是 Object.prototype.toString.call(value) === "[object Array]"

类型判断

typeof

A instanceof Array

toString.call

splice

功能:删除、替换、插入

参数:开始位置;删除个数;添加到数组的元素;

reduce

// 去重
const myArray = ["a", "b", "a", "b", "c", "e", "e", "c", "d", "d", "d", "d"];

const myArrayWithNoDuplicates = myArray.reduce((acc, cur) => {
	if (!acc.includes(cur)) {
		acc.push(cur);
	}
	return acc;
}, []);

改变原数组的方法

push、pop、shift、unshift;sort、reverse;splice;

其他:concat、slice、join、indexOf、lastIndexOf;forEach、map、filter;some、every、find、findIndex;

对象

Object.create(proto)

创建新对象,新对象的 prototype 【由传入的参数指定】,参数 proto 必须是 null 或对象,否则会报错;

Object.create() 和 new Constructor() 的区别

Object.create() 创建的对象实例【具有独立的原型链】,可以独立地管理和修改属性;

new Constructor() 创建的对象实例共享相同的原型链,可能导致意外的副作用和属性共享问题;

代码解释 ↓↓

function Person(name) {
	this.name = name;
}

Person.prototype.greet = function () {
	console.log(`Hello, my name is ${this.name}.`);
};

// 使用 new Constructor() 创建两个对象实例
const person1 = new Person("Alice");
const person2 = Object.create(Person);

// 修改 Person 的原型上的属性
Person.prototype.age = 30;

// person2 不会被影响到
console.log(person1.age); // 输出: 30
console.log(person2.age); // 输出: undefined

继承

function Parent() {
	this.name = "parent";
}
function Children() {
	Parent.call(this); // 调用父类构造函数
	this.type = "Child";
}
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;

为对象创建原型

const proto = { foo: "bar" };
const obj = Object.create(proto);
obj.__proto__ === proto;

克隆对象

function clone(origin) {
	return Object.create(Object.getPrototypeOf(origin));
}

DefineProperty

在对象上定义一个新属性,或修改其现有属性,并返回此对象;

优点:更精确,添加更多细节(可写、可枚举、可配置、自定义 get、set 等,修改默认行为);

configurable 和 writable 的区别

configurable 是否可配置,不能再次使用 defineProperty 重新定义之前已经定义过的属性;

writable 是否可写,是否能修改属性值 obj.x = 20

Proxy

请解释一下 Proxy 对象的作用和用法?

用于拦截并控制目标对象的访问,允许在访问对象之前和之后执行自定义的操作。它接受两个参数:目标对象和一个处理程序对象(handler),其中处理程序对象定义了拦截的行为。通过 Proxy,可以对目标对象的属性访问、属性赋值、函数调用等进行拦截和处理。

请说明 Proxy 中的 get 和 set 拦截器的作用和用法。

get 拦截器:用于拦截对象属性的访问操作,当访问代理对象的属性时触发。

set 拦截器:用于拦截对象属性的赋值操作,给代理对象的属性赋值时触发。

通过在处理程序对象中定义 get 和 set 方法,可以自定义拦截的行为,并在拦截发生时执行相应的操作,如打印日志、进行验证等。

let person = {
	name: "Alice"
}
let proxy = new Proxy(person, {
	get: function(target, prop) {
		console.log(`访问属性:${prop}`);
		return target[prop];
	}
	set: function(target, prop, value) {
		console.log(`设置属性:${prop} = ${value}`);
		target[prop] = value;
	}
})
const proxy = new Proxy(target, handler);
console.log(proxy.name); // 输出: 访问属性:name,Alice
proxy.age = 35; // 输出: 设置属性:age = 35
console.log(proxy.age); // 输出: 访问属性:age,35

// receiver:proxy 是真实对象的替身,如果想要【获取真实对象】的属性和状态,需要将真实对象传递给代理对象作为参数,receiver 就是这个参数,它告诉代理对象要去向谁获取属性值。也相当于上下文。

Proxy 中的 has 方法的作用是什么?

用于拦截使用 propKey in proxy 的操作,判断属性是否存在于代理对象中。当在代理对象上进行属性存在性检查时,has 方法会被触发,并返回一个布尔值,表示属性是否存在。通过定义 has 方法,可以自定义拦截操作并返回相应的结果。

请解释一下 Proxy 中的 apply 和 construct 拦截器的用途?

apply 拦截器用于拦截 Proxy 实例作为函数调用时的操作,即当代理对象被调用时触发。通过定义 apply 方法,可以自定义拦截函数调用的行为,并在调用发生时执行自定义的操作,如参数验证、记录日志等。

construct 拦截器用于拦截 Proxy 实例作为构造函数调用时的操作,即当使用 new 关键字创建代理对象实例时触发。通过定义 construct 方法,可以自定义拦截构造函数调用的行为,并在构造函数调用发生时执行自定义的操作,如创建实例前的准备工作、实例化对象的类型检查等。

Proxy 还支持哪些其他的拦截器方法?

deleteProperty:用于拦截 delete proxy[propKey] 的操作,当删除代理对象的属性时触发。

defineProperty:用于拦截 Object.defineProperty(proxy, propKey, propDesc) 的操作,当在代理对象上定义属性时触发。

getPrototypeOf:用于拦截 Object.getPrototypeOf(proxy) 的操作,当获取代理对象的原型时触发。

setPrototypeOf:用于拦截 Object.setPrototypeOf(proxy, newProto) 的操作,当设置代理对象的原型时触发。

高级功能

属性验证和保护;

数据绑定(拦截 get 和 set 操作,构建响应式界面);

缓存(调用拦截方法并缓存结果);

代理远程服务(调用拦截方法,把方法调用转发到远程服务,获取响应结果)

Reflect

提供了一组访问对象底层操作的方法,和 Proxy 提供的方法是完全一致的,Proxy 是代理对象,它不修改源对象,因此 Reflect 可以和 Proxy 配合使用,让源对象也发生改变。它的优势是比较优雅,写法和 Proxy 保持一致,而且更安全(this 指向问题)

浅克隆

1、Object.assign

2、展开运算符

3、arr.slice(数组)

深克隆

1、JSON.parse(JSON.stringify(obj))

2、递归

function deepClone(obj, memo = new WeakMap()) {
	// 如果是基本数据类型或 null,则直接返回
	if (obj === null || typeof obj !== "object") {
		return obj;
	}
	// 如果对象被拷贝过,直接返回之前的拷贝结果,避免循环引用
	// 循环引用,一个对象包含自身的引用或者多个对象互相引用的情况
	// 如果不处理循环引用,深拷贝可能会陷入无线递归
	// 比如 objA.prop = objB; objB.prop = objA;
	// 一个对象引用了另一个对象,后者又直接或间接引用了前者
	if (memo.has(obj)) {
		return memo.get(obj);
	}
	// 检查memo中是否已经存在了obj。这是通过memo.has(obj)来判断的。
	// 如果memo中已经有了obj的记录,说明这个对象已经被拷贝过,就直接返回memo中对应的拷贝结果,
	// 而不再递归深拷贝这个对象
	// 特殊对象处理,如 Date 和 RegExp
	if (obj instanceof Date) {
		return new Date(obj);
	}
	if (obj instanceof RegExp) {
		return new RegExp(obj);
	}
	// 处理数组和对象
	const newObj = Array.isArray(obj) ? [] : {};
	memo.set(obj, newObj);

	// 递归拷贝子对象
	for (let key in obj) {
		newObj[key] = deepClone(obj[key], memo);
	}
	return newObj;
}

/**
1. 如果是基本数据类型或 null,则直接返回
2. 特殊对象处理(date RegEXP等),instanceof 判断,然后返回 new Date(obj)
3. 数组和对象的处理,判断是不是数组,是数组就新建一个[],否则就是{}
4. 递归拷贝子对象,重复之前的步骤
5. 循环引用的问题:如果对象被拷贝过,直接返回之前的拷贝结果,避免循环引用。这时需要用一个 weakmap 来管理这些数据。
 */

函数

参数柯里化

将接受多个参数的函数,转化为一系列【接受单一参数的函数】的过程

function add(x, y) {
	return x + y;
}
// 柯里化
function add(x) {
	return function (y) {
		return x + y;
	};
}

function add() {
	// 取得所有参数
	const args = [...arguments];
	if (args.length >= 2) {
		// 如果参数足够,则直接返回它们的和
		return args.reduce((a, b) => a + b);
	} else {
		return function () {
			// 参数不足,返回新函数,等待更多参数
			return add(...args, ...arguments);
		};
	}
}

Call / Apply / Bind

用途:函数调用和上下文绑定

call - 显式指定函数的上下文(即 this 值)和参数,我们可以使用 call 方法把一个对象传递给函数,让函数在该对象的上下文中执行。

apply - 类似 call 方法,不同点是参数必须以数组的形式传递给函数,而不是逐个传入

bind - bind 方法不会立即执行函数,而是创建一个新的函数,该函数的 this 值被绑定到指定的对象。此外,我们可以在 bind 方法中指定参数,这些参数将作为绑定函数的预设参数,不会被改变。

// 绑定 this
this.x = 9
const moduleS = {
  x: 81,
  getX() {
    return this.x
  }
}
console.log(moduleS.getX())

const retrieveX = moduleS.getX
console.log(retrieveX())
const boundGetX = retrieveX.bind(moduleS)
console.log(boundGetX())

// 预设函数的参数
function addx, y){
	return x + y;
}
const add5 = add.bind(null, 5)
console.log(add5(3)) // 输出8

// 柯里化
function mutiply(a, b) {
	return a * b;
}
const multiplyByTwo = multiply.bind(null, 2);
console.log(multiplyByTwo(4))

立即执行函数

声明时立即执行的函数;

(function(){
	// 代码块
}){}

特点:

1、独立作用域

2、保护内部变量(对外不可见)

3、实现模块模式(返回公共接口,隐藏内部实现细节)

4、执行一些初始化操作

ES6

Symbol

值唯一且不可变,通过 Symbol 函数生成,每个 Symbol 值不存在相等性比较

主要用途

给对象定义唯一的属性名(防止属性名冲突),定义类的私有属性和方法(不可迭代)

Symbol 转换

Symbol(’foo’).toString

Symbol(’foo’).description

Set / Map / WeakMap / WeakSet

Set

集合;值的合集;集合中的元素只会出现一次(唯一性);

迭代 - 会按照插入顺序访问集合中的元素;

array 转 set:new Set(arr)

set 转 array:[...setArr]

方法

add;clear;delete;has;size;keys/values/entries;forEach;

Set 和 Array 的区别

  1. 无重复值
  2. 没有索引,不能通过下标访问
  3. 没有 push 和 pop 方法,但有 add 和 delete 方法;

比较

需要存储唯一值,且不关心顺序,可以使用 Set;需要有序的、可以重复的集合,并且需要频繁的执行操作(两端添加或删除),可以使用 Array;

const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);

const intersection = new Set([...setA].filter((x) => setB.has(x)));
const union = new Set([...setA, ...setB]);
const difference = new Set([...setA].filter((x) => !setB.has(x)));

Map

键值对集合;集合中的每个键只能出现一次;

方法

set;get;clear;delete;has;size;keys/values/entries;forEach

Object 和 Map 的区别

  1. Map 的键可以是任何值,Object 只能是 String 或 Symbol
  2. Map 的键按照插入顺序迭代
  3. Map 的项目数量通过 size 属性获取,Object 需要通过 Object.key().length
  4. Map 是可迭代对象,用 forEach、for-of 来遍历,Object 可以使用 Object.entries 或 for-in 迭代;
  5. Map 的性能更好,赋值和搜索的复杂度是 O(n)

for-in 和 for-of 的区别

for-in:遍历对象的可枚举属性,可以遍历对象的键(输出属性名),遍历数组时,会遍历数组的可枚举属性,包括原型链上的属性;

for-of:遍历可迭代对象的元素,包括数组、字符串、set、map,不支持遍历 Object(不是可迭代对象)

WeakMap

弱引用(如果没有其他地方引用该对象,它可以被垃圾回收器回收)

键必须是对象,不能是原始类型的值;

特点:键是弱引用的,当键对象失去其他引用时,它可以被垃圾回收机制回收

原理

Map 在 Javascript 中的实现相当于使用了两个数组(存放键,存放值),缺点是会导致内存泄露,因为数组会一直引用每个键和值。这种引用会使垃圾回收算法不能回收处理它们。WeakMap 就是解决这个问题的,如果一个键没有在其他地方使用,它和值之间的关系就是弱引用,可以被垃圾回收掉。

WeakMap 不可遍历,也没有 size 属性,因为其中的元素可能会突然消失。

场景

WeakMap 适合临时数据、对象状态等短期存储(临时缓存)

DOM 节点,DOM 节点被删除时,元数据也会被垃圾回收

登录状态,退出登录后被垃圾回收

Map 适合持久化的、需要快速查找的数据

用户信息;键可以是另一个对象(表达关系);时间线(保持顺序)

Object:更自由的操作数据,适合频繁操作的小规模数据