前端面试八股文 JavaScript

发布时间 2023-10-10 14:24:33作者: Betrayer

前端面试八股文 JavaScript

谈谈对原型链的理解

在JavaScript中,每个对象都有一个原型对象proto,指向其构造函数的原型对象prototype。

当我们创建一个新的实例对象时,这个对象会从其构造函数的原型对象prototype中继承属性和方法。如果实例对象自身没有某个属性或方法,但是其构造函数原型对象prototype中存在,那么这个对象会从其构造函数的原型对象prototype中查找并继承这个属性或方法。

每个对象的原型链上可以包含多个对象,这些对象都有指向它们的构造函数prototype的proto。因此,当我们访问一个对象的属性或方法时,如果该对象没有这个属性或方法,JavaScript引擎会在原型链上进行查找,直到找到这个属性或方法或者到达原型链的末尾。

Object.prototype.__proto__ == null

总之,原型链是JavaScript中实现对象和函数继承的核心机制,通过原型链,我们可以实现代码的重用和减少内存占用,同时也可以在不改变现有代码的情况下添加新的属性和方法。

JavaScript如何实现继承

  1. 原型继承

    function SuperType() {
    	this.property = true;
    }
    
    SuperType.prototype.getSuperValue = function () {
    	return this.property;
    }
    
    // 继承SuperType
    function SubType() {
    	this.subProperty = false;
    }
    
    SubType.prototype = new SuperType();
    
    SubType.prototype.getSubValue = function () {
    	return this.subProperty;
    }
    

    原型继承的实现:子类的prototype = 父类的实例

    原型继承特点:可以继承父类的原型属性

    原型继承的缺点:继承的属性都是原型属性,不能继承私有属性

  2. 借用构造函数继承

    function SuperType(property) {
    	this.property = property;
    }
    
    function SubType(property) {
    	SuperType.call(this, property);
    }
    
    var instance1 = new SubType(false);
    console.log(instance1.property); // false
    
    var instance2 = new SubType(true);
    console.log(instance2.property); // true
    

    借用构造函数继承的实现:在子类构造函数中使用call/apply调用父类构造函数,将父类构造函数指向子类实例。

    借用构造函数继承的特点:可以继承父类的私有属性。

    借用构造函数继承的缺点:只能继承父类的私有属性,不能继承父类的原型属性。

  3. 组合继承

    function SuperType(property) {
    	this.property = property;
    }
    
    SuperType.prototype.getSuperValue = function () {
    	return this.property;
    }
    
    function SubType(property, subProperty) {
    	SuperType.call(this, property);
     	this.subProperty = subProperty;
    }
    
    SubType.prototype = new SuperType(false);
    
    SubType.prototype.getSubValue = function () {
    	return this.subProperty;
    }
    

    组合继承的实现:同时使用原型继承和借用构造函数继承

    组合继承的特点:1. 子类可以继承父类原型属性和私有属性

    组合继承的缺点:对于父类的私有属性,子类继承时候同时存在于私有属性和原型属性中,造成了冗余

  4. 寄生组合式继承(最佳实践)

    function getSuperTypeProtoType(SuperType) {
        function Func() {};
        Func.prototype = SuperType.prototype;
        return new Func();
    }
    
    function SuperType(property) {
    	this.property = property;
    }
    
    SuperType.prototype.getSuperValue = function () {
    	return this.property;
    }
    
    function SubType(property, subProperty) {
    	SuperType.call(this, property);
     	this.subProperty = subProperty;
    }
    
    SubType.prototype = getSuperTypeProtoType(SuperType);
    
    SubType.prototype.getSubValue = function () {
    	return this.subProperty;
    }
    
  5. ES6的class

    class Parent {  
        constructor() {  
            this.name = 'Parent';  
        }  
    }  
      
    class Child extends Parent {  
        constructor() {  
            super(); // 调用父类的构造函数  
            this.name = 'Child';  
        }  
    }  
      
    var child = new Child();  
    console.log(child.name); // 输出:Child  
    console.log(child.constructor); // 输出:class Child extends Parent {...}
    

JavaScript有哪些数据类型

基本数据类型(7种):Number、String、Boolean、Null、 Undefined、Symbol(ES6)、BigInt(ES11)

引用数据类型(1种):Object

number类型最大值是多少?如果后台发的数据超过这个值怎么办?

大多数浏览器最大值是253,最小是-253。最大值和最小值可以通过Number.MAX_VALUE和Number.MIN_VALUE查看。

后台发送数据超过这个值可以用字符串类型代替。

如何判断一个变量是否为数组?

const arr = [];
// Array.isArray
console.log(Array.isArray(arr));
// Object.prototype.toString
console.log(Object.prototype.toString.call(arr) === '[object Array]');
// instanceof Array
console.log(arr instanceof Array);

Null和Undefined的区别

null表示引用类型的对象为空,undefined则表示变量未定义,null == undefined

typeof null; // object
typeof undefined; // undefined

Number(null); // 0
Number(undefined); // NaN

call、apply、bind的区别

call、bind和apply都是JavaScript中用来改变函数内部this指向的方法,但它们之间存在一些明显的区别。

传参方式:call和apply在传参方式上有所不同。call方法是依次传入参数,每个参数作为一个独立的参数。而apply方法则将所有参数打包成一个数组传递。

例如:

func.call(obj, 1, 2, 3);  // call方法传参  
func.apply(obj, [1, 2, 3]);  // apply方法传参

返回值:call和apply方法都会执行函数并返回结果,而bind方法不会立即执行函数,而是返回一个新的函数。

例如:

javascript复制代码

var newFunc = func.bind(obj);  // bind方法返回新函数

this指向:call、bind和apply方法都可以用来改变函数内部的this指向。但在使用bind方法时,如果省略了第二个参数(this),那么bind会绑定当前函数运行时所在的环境(即全局作用域或函数作用域),而不是指定的上下文。

例如:

func.call(obj);  // this指向obj  
func.apply(obj);  // this指向obj  
var newFunc = func.bind();  // this指向全局作用域或函数作用域(取决于环境)

柯里化实现:使用bind方法可以实现柯里化,即将函数转化为接收部分参数的函数。这意味着可以将后续的参数以一个一个的方式传入,而不是一次性传入所有参数。

例如:

var add = function(x, y) { return x + y; };  
var partialAdd = add.bind(null, 10);  // partialAdd是一个新函数,它接受y作为参数,将x设为10  
console.log(partialAdd(20));  // 输出30

防抖和节流的概念,实现防抖和节流

防抖(debounce)和节流(throttle)是两种常用的优化手段,主要用于限制函数的执行频率。

防抖:如果你持续触发一个事件,防抖的作用就是在一定时间段内,事件处理函数只执行一次,如果在这个时间段内又触发了这个事件,那么会重新计算执行时间。常用于输入框实时搜索,用户在输入内容的过程中,只有当用户停止输入一段时间后,才去执行事件。

简单的代码实现如下:

function debounce(func, wait) {  
    let timeout;  
    return function() {  
        let context = this;  
        let args = arguments;  
        clearTimeout(timeout);  
        timeout = setTimeout(function() {  
            func.apply(context, args);  
        }, wait);  
    };  
}

节流:如果你持续触发一个事件,节流的作用就是在一定时间内只让事件处理函数执行一次。常用于滚动加载,时间戳更新等功能。

简单的代码实现如下:

function throttle(func, limit) {  
    let inThrottle;  
    return function() {  
        let context = this;  
        let args = arguments;  
        if (!inThrottle) {  
            func.apply(context, args);  
            inThrottle = true;  
            setTimeout(function() {  
                inThrottle = false;  
            }, limit);  
        }  
    };  
}

深拷贝、浅拷贝的区别?如何实现深拷贝和浅拷贝?

赋值不属于拷贝

let arr = [1, 2, 3];
let arr1 = arr; // 这里仅仅是把数组的内存地址赋值给arr1,这里不叫拷贝

在 JavaScript 中,深拷贝和浅拷贝的主要区别在于它们复制对象时如何处理对象内部的子对象。

  • 浅拷贝(Shallow Copy):创建一个新的对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存中的地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
  • 深拷贝(Deep Copy):创建一个新的对象,复制过来的对象的属性值,如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,那么会递归地去深拷贝这个引用的对象。

在 JavaScript 中实现深拷贝和浅拷贝的方法如下:

浅拷贝

  1. 使用 Object.assign() 方法:
let original = {a: 1, b: 2};  
let copy = Object.assign({}, original);
  1. 使用扩展运算符(...):
let original = {a: 1, b: 2};  
let copy = {...original};

深拷贝

  1. 使用 JSON 的 stringify 和 parse 方法:
let original = {a: 1, b: 2, c: {d: 3}};  
let copy = JSON.parse(JSON.stringify(original));

注意:这种方法只适用于可序列化的对象,如果对象中包含函数、undefined 或者 symbol 类型,或者存在循环引用的情况,这种方法就不能使用。

  1. 通过递归复制所有层级实现
  2. 使用第三方库,如 lodash 的 _.cloneDeep() 方法:
let _ = require('lodash');  
let original = {a: 1, b: 2, c: {d: 3}};  
let copy = _.cloneDeep(original);

注意:lodash 的 _.cloneDeep() 方法可以处理复杂对象和循环引用的情况,但是它不能处理函数、undefined 或者 symbol 类型。

var、const、let

在JavaScript中,varconstlet都是用来声明变量的关键字,但它们之间有一些重要的区别:

  1. 作用域var在声明变量时有函数级作用域,而letconst有块级作用域。这意味着var定义的变量在整个函数内部都可见,而letconst定义的变量只在声明它们的代码块内可见。
  2. 重声明:在同一个作用域内,你可以多次使用var来声明同一个变量,但不能用letconst多次声明同一个变量。
  3. 提升:JavaScript中有一个概念叫做“变量提升”(Hoisting),意思是在执行代码之前,解释器会先读取所有的变量声明。var声明的变量会被提升,意味着你可以在声明之前使用它们,此时变量的值为undefined。但是letconst声明的变量虽然也会被提升,但是你不能在声明之前访问它们,否则会报错。这个区间被称为“暂时性死区”。
  4. 赋值letconst声明的变量不能重新赋值(对于const),或者重新声明(对于letconst)。但是var可以。
  5. 全局作用域行为:在全局作用域中声明的变量,varlet会成为全局对象(在浏览器中是window对象)的属性,但const不会。

总结一下,varletconst关键字都可以用来声明变量,但它们的行为略有不同。在现代JavaScript编程中,建议使用letconst,因为它们的作用域更易理解,有利于防止一些常见的编程错误。对于那些不应该改变的变量,应使用const来声明。

箭头函数和普通函数的区别

JavaScript中的箭头函数和普通函数之间有几个主要的区别:

  1. this关键字的绑定:在普通函数中,this关键字的值取决于函数的调用方式。例如,如果一个函数作为一个对象的方法被调用,那么this将绑定到这个对象上。如果函数只是被简单地调用(不是作为对象的方法),this通常绑定到全局对象(在浏览器中是window)上。然而,在箭头函数中,this关键字的值在函数被创建时就确定了,它的值就是创建时所在的上下文的this。换句话说,箭头函数不会创建自己的this上下文,所以它内部的this实际上来自于外层代码块。
  2. arguments对象:在普通函数中,你可以使用特殊的arguments对象访问函数的参数。在箭头函数中,arguments对象不可用。如果你需要类似的行为,你需要使用rest参数。
  3. new.target和new.target.prototype:在普通函数中,new.targetnew.target.prototype是可用的,它们用于指示函数是否作为构造函数被调用。在箭头函数中,这两个属性不可用。
  4. prototype属性:普通函数有一个prototype属性,这个属性是一个对象,你可以在这个对象上添加方法,这些方法将被所有这个函数创建的对象实例继承。箭头函数没有prototype属性。
  5. call()、apply()和bind()方法:普通函数有call()apply()bind()方法,这些方法用于改变函数调用的上下文(即this的值)。箭头函数没有这些方法。
  6. 生成器函数和异步函数:普通函数可以被声明为生成器函数或异步函数。箭头函数不能被声明为生成器函数或异步函数。
  7. 语法:箭头函数的语法更简洁,不需要使用function关键字,只需要一个箭头(=>)。这使得它们更适合于简短、一行内的函数。

ES6的新特性

ES6(ECMAScript 6)是JavaScript语言的一个重要版本,在ES6中,有许多新的特性被引入,以下是一些主要的特性:

  • 块级作用域(Let,Const)。ES6引入了块级作用域的概念,这意味着你可以使用letconst关键字来声明变量和常量,它们只在声明它们的代码块内可见。

  • 定义类的语法糖(Class)。ES6引入了定义类的语法糖,这使得创建和管理对象变得更加简单和直观。

  • 一种基本数据类型(Symbol)。ES6引入了一种新的基本数据类型——Symbol,它是一种唯一的、不可变的、不可打印的特殊类型。

  • 变量的解构赋值。ES6引入了解构赋值,这使得将数组或对象的属性提取到变量中变得更加简洁和方便。

    const user = { name: 'xiaoming', age: 18 };
    const { name, age } = user;
    console.log(name);
    console.log(age);
    
  • 函数参数允许设置默认值,引入了Rest参数,新增了箭头函数。ES6允许在函数参数中设置默认值,同时引入了Rest参数和箭头函数,这使得编写函数变得更加灵活和方便。

  • 数组新增了一些API,如isArray、from、of方法;数组实例新增了entries(),keys(),values()等方法。ES6为数组新增了一些API和方法,使得操作和处理数组变得更加简单和高效。

  • 对象和数组新增了扩展运算符。ES6在对象和数组中新增了扩展运算符(...),这使得将数组或对象的元素展开到另一个数组或对象中变得更加简单和方便。

  • ES6新增了模块化(Import/Export)。ES6引入了模块化的概念,使得JavaScript代码可以以模块的形式进行编写和组织,增强了代码的可维护性和可重用性。

  • ES6新增了Set和Map数据结构。ES6引入了Set和Map数据结构,它们都是基于JavaScript对象,用于存储和操作键值对的集合。

除此之外,ES6还有其他一些新特性,如生成器函数和遍历器函数、Proxy构造函数等。这些新特性可以帮助开发人员编写更加高效、简洁和易于维护的代码,推动了JavaScript语言的发展和应用。

使用new创建对象的过程是什么样的?

在JavaScript中,使用new关键字创建对象的过程包括以下步骤:

  1. 创建一个新的空对象:首先,JavaScript会创建一个新的空对象。这个对象是新创建的对象,该对象的类型与构造函数的类型相同。
  2. 设置原型链:新创建的对象的[[Prototype]](即__proto__)会被设置为构造函数的prototype对象。这意味着新对象可以访问构造函数原型上的属性和方法。
  3. 构造函数执行:之后,构造函数会使用新创建的对象作为其上下文被执行。换句话说,this关键字在构造函数内部指向新创建的对象。
  4. 返回新对象:除非构造函数显式地返回一个非原始值(即对象或函数),否则新创建的对象将被返回。如果构造函数返回了一个非原始值,那么这个非原始值将会替代构造函数返回的新对象。

手写bind函数

Function.prototype.myBind = function (context, ...args1) {
    let orginFunc = this;
    return function (...args2) {
        orginFunc.apply(context, [...args1, args2]);
    }
}

谈谈对闭包的理解?什么是闭包?闭包有哪些应用场景?闭包有什么缺点?如何避免闭包?

闭包是指有权访问另一个函数作用域中的变量的函数。在JavaScript中,闭包是一个非常常见和有用的概念,它有以下几个特点:

  1. 闭包可以读取函数内部的变量,即使这些变量在函数外部定义。
  2. 闭包可以把变量始终保存在内存中,即使用户离开了这个闭包,这些变量仍然可以被访问。

闭包在JavaScript中的应用场景非常广泛。例如,在创建对象时,可以使用闭包来定义对象的私有属性和方法,以确保这些属性和方法不会被外部访问。此外,闭包还可以用于创建函数工厂和模块化代码。

// 创建一个函数工厂  
function createMultiplier(multiplier) {  
    // 这是一个闭包,它记住了multiplier的值  
    return function(num) {  
        return num * multiplier;  
    };  
}  
  
// 使用函数工厂创建新的函数  
var double = createMultiplier(2);  
var triple = createMultiplier(3);  
  
console.log(double(5));  // 输出 10  
console.log(triple(5));  // 输出 15 

当然,闭包也存在一些缺点,需要谨慎使用。首先,由于闭包会占用更多的内存,因此如果使用不当,可能会导致网页性能变差,甚至在IE下容易造成内存泄露。其次,由于闭包可以访问全局变量,因此如果不小心将全局变量设置为不可读(即使用const声明),则可能会导致代码无法正常运行。

为了避免闭包带来的问题,可以考虑以下几点:

  1. 尽量避免在循环中创建闭包,因为这可能会导致内存泄漏。
  2. 在不需要使用闭包的情况下,尽量避免使用闭包。例如,如果只需要返回一个简单的值,就没有必要使用闭包。
  3. 在使用闭包时,要注意内存管理,及时释放不需要的内存。例如,可以使用垃圾回收机制来自动释放不再使用的变量的内存。
  4. 避免在闭包中访问不可读的全局变量,以免造成代码无法正常运行。

谈谈对事件循环的理解?

  • 事件循环(Event Loop)是 JavaScript 单线程执行模型的核心机制,用于管理和调度异步任务、事件和回调函数的执行顺序,它不断地从任务队列中获取任务并执行。
  • 事件循环有一个微任务队列(Micro Task)和一个宏任务队列(Macro Task)。
  • 宏任务:较大的任务单元,通常由 I/O 操作、定时器回调、UI 渲染等触发。每次事件循环只执行一个宏任务,按顺序执行。宏任务包括:script(全局任务), setTimeout, setInterval, setImmediate,I/O, UI rendering等。
  • 微任务:较小的任务单元,优先级高于宏任务。微任务包括:Promise, process.nextTick, MutationObserver等。
  • 事件循环的基本流程:
    1. 执行当前的宏任务(例如定时器回调、I/O 操作、UI 渲染等)。
    2. 执行所有微任务队列中的微任务。
    3. 更新页面渲染(如果需要)。
    4. 执行下一个宏任务。
  • 微任务的执行时机:在当前宏任务执行完毕后立即执行,确保了微任务的优先级和快速执行。
  • 事件循环机制允许 JavaScript 在单线程中处理异步操作,避免了阻塞整个程序的执行,提高了程序的响应性能。
  • 合理使用异步编程模式(如回调函数、Promise、async/await)可以更好地利用事件循环,处理并发任务和避免阻塞。

谈谈对Promise的理解?

Promise是JavaScript中用于处理异步操作的一种解决方案。它表示一个最终可能完成(成功解决)或失败(被拒绝)的事件及其结果的值。

Promise有三种状态:

  1. pending(进行中):初始状态,能够转移到 fullfilled 或者 rejected 状态
  2. resolved(已完成,又称fulfilled):意味着操作成功完成,不可转移状态
  3. rejected(已失败):意味着操作失败,不可转移状态

Promise实例的状态改变只有两种可能:从pending变为resolved和从pending变为rejected。

使用Promise可以避免回调地狱(callback hell),减少回调嵌套,并使异步代码看起来像同步代码一样直观易懂。

Promise是异步编程的一种改进方式,它通过封装异步任务并提供承诺结果,使得异步操作的可读性和易维护性得到提升。

一个Promise不能被取消,一旦新建它就会立即执行,无法中途取消。如果Promise的回调函数没有设置错误处理,那么即使任务失败,也不会对外部产生影响。同时,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

总的来说,Promise为JavaScript的异步编程提供了一种很有用的方式,使得我们能够更方便、更直观地处理异步操作。

Promise中回调函数是同步的还是异步的?then的链式调用是同步的还是异步的?

Promise中的回调函数是异步的。当您使用Promise时,您通常在其构造函数中提供一个处理函数,该处理函数接受两个参数:一个用于处理成功结果的回调函数(resolve),另一个用于处理错误的回调函数(reject)。这些回调函数是异步的,意味着它们不会立即执行,而是在Promise的状态变为“resolved”或“rejected”时执行。

然而,then方法的链式调用是同步的。当您调用then方法时,它会立即返回一个新的Promise。这个新Promise的状态取决于原始Promise的状态。如果原始Promise已经解决,那么新Promise也会立即解决。如果原始Promise被拒绝,那么新Promise也会立即被拒绝。这种行为是同步的,因为它会立即发生,而不会等待任何异步操作完成。

需要注意的是,虽然then方法的调用是同步的,但是传递给then的回调函数是异步执行的。

柯里化是什么?有什么用?怎么实现?

柯里化(Currying)是一种在函数式编程中常见的技术,它将一个多参数的函数转换成一系列使用一个参数的函数。这个技术可以使得函数调用更加灵活,可以用于部分应用(Partial Application)和函数复合(Function Composition)等多种场景。

柯里化的实现:

1.通用实现

function sum (a, b, c, d) {
    return a + b + c + d;
}
function currying (fn, a, b) {
    return fn.bind(null, a , b);
}
const curriedSum = currying(sum, 1, 2);
curriedSum(3, 4);

2.递归实现

let sum = function (a, b, c, d) {
    return a + b + c + d;
}
let currying = function (fn) {
    const len = fn.length;
    let _args = [];
    const curry = () => {
        return function (...args) {
            // 如果参数攒够了就执行
            if (_args.length + args.length >= len) {
                const result = fn( ..._args, ...args);
                // 执行完重置_args
                _args = [];
                return result;
            }
            // 参数不够就继续攒
            else {
                _args = [..._args, ...args];
                return curry();
            }
        }
    }
    return curry();
}
const curriedSum = currying(sum);
curriedSum(1)(2)(3, 4);
let currying = function (fn) {  
  return function curried(...args) {  
    // 如果当前传入参数长度小于原函数需要的参数长度,则返回新的函数等待接收新的参数  
    if (args.length < fn.length) {  
      return function(...args2) {  
        return curried(...args.concat(args2));  
      }  
    }  
    // 参数数量满足,执行原函数  
    else {  
      return fn.apply(null, args);  
    }  
  };  
}

经典面试题

// 实现一个add方法,使计算结果能够满足如下预期:
// add(1)(2)(3) == 6 // true
// add(1, 2, 3)(4) == 10 // true
// add(1)(2)(3)(4)(5) == 15 // true

function add() {
  // 第一次执行时,定义一个数组专门用来存储所有的参数
  var _args = Array.prototype.slice.call(arguments);

  // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
  var _adder = function() {
    _args.push(...arguments);

    return _adder;
  };

  // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
  _adder.toString = function () {
    return _args.reduce(function (a, b) {
    	return a + b;
    });
  }
  return _adder;
}

CommonJs和Es Module的区别

CommonJS和ESM是JavaScript中两种不同的模块系统。二者都是为了解决以下问题:

  • 解决变量污染问题,每个文件都是独立的作用域,所以不存在变量污染
  • 解决代码维护问题,一个文件里代码非常清晰
  • 解决文件依赖问题,一个文件里可以清楚的看到依赖了那些其它文件

CommonJs

  • 最早出现在JavaScript社区
  • CommonJs可以动态加载语句,代码发生在运行时
  • CommonJs混合导出,还是一种语法,只不过不用声明前面对象而已,当导出引用对象时,之前的导出就被覆盖了
  • CommonJs导出值是拷贝,可以修改导出的值,这在代码出错时,不好排查引起变量污染

Es Module

  • Es6引入的概念
  • Es Module是静态的,不可以动态加载语句,只能声明在该文件的最顶部,代码发生在编译时
  • Es Module混合导出,单个导出,默认导出,完全互不影响,在导入时如果要同时使用,需要先导入默认的
  • Es Module导出是引用值之前都存在映射关系,并且值都是可读的,不能修改

讲讲JavaScript的垃圾回收

JavaScript的垃圾回收机制主要有两种:标记清除和引用计数。

  1. 标记清除(Mark-and-sweep):这是JavaScript中主流的垃圾回收算法。在标记清除过程中,垃圾收集器会定期扫描内存中的对象,将可达对象和不可达对象分别标记为“存活”和“垃圾”。然后,垃圾收集器从内存中清除被标记为“垃圾”的对象,从而释放它们所占用的内存空间。
  2. 引用计数(Reference counting):这是JavaScript中较少使用的垃圾回收算法。引用计数是通过计算对象在内存中的位置,决定垃圾的数量,然后决定是否回收该对象。每当一个对象被引用时,其引用计数会增加;每当一个对象的引用被解除时,其引用计数会减少。当一个对象的引用计数为0时,表示没有任何引用指向该对象,该对象变为不可达,因此可以将其回收。

JavaScript的垃圾回收机制是为了防止内存泄漏,就是寻找到不再使用的变量,并释放掉它们所指向的内存。

实现发布订阅模式demo

class Observer {  
    constructor(name) {  
        this.name = name;  
    }  

    update(message) {  
        console.log(`${this.name} received: ${message}`);  
    }  
}  

class Topic {  
    constructor() {  
        this.observers = [];  
    }  

    subscribe(observer) {  
        this.observers.push(observer);  
    }  
  
    publish(message) {  
        this.observers.forEach(observer => observer.update(message));  
    }  
}
let topic = new Topic();  

let observer1 = new Observer("Observer 1");  
let observer2 = new Observer("Observer 2");  
let observer3 = new Observer("Observer 3");  

topic.subscribe(observer1);  
topic.subscribe(observer2);  
topic.subscribe(observer3);  

topic.publish("Hello, observers!");

实现数组拍平

数组拍平也称数组扁平化,就是将数组里面的数组打开,最后合并为一个数组,实现方式如下:

  • Array.prototype.flat

    传入一个整数参数,整数即“拉平”的层数
    传入 <=0 的整数将返回原数组,不“拉平”
    Infinity 关键字作为参数时,无论多少层嵌套,都会转为一维数组

    const arr = [1, [2, 3], [4, [5, [6]], 7]];
    // 传入一个整数参数,整数即“拉平”的层数
    arr.flat(2);
    // [1, 2, 3, 4, 5, [6], 7]
    
    // 传入 <=0 的整数将返回原数组,不“拉平”
    arr.flat(0);
    arr.flat(-10);
    // [1, [2, 3], [4, [5, [6]], 7]];
    
    // Infinity 关键字作为参数时,无论多少层嵌套,都会转为一维数组
    arr.flat(Infinity);
    // [1, 2, 3, 4, 5, 6, 7]
    
  • toString

    toString 方法是一个比较简单的方法,他是把数组每个元素转化成字符串之后直接拍平
    然后通过 map 遍历每个元素,让其变成 Number 类型

    const arr = [1, [2, 3], [4, [5, [6]], 7]];
    arr.toString(); // '1,2,3,4,5,6,7'
    arr.toString().split(',');
    arr.toString().split(',').map(item => Number(item));
    
  • join

    和toString方法类似,思路都是将数组转化成字符串再分割,最后转换数据类型

    const arr = [1, [2, 3], [4, [5, [6]], 7]];
    arr.join(); // '1,2,3,4,5,6,7'
    arr.join().split(',');
    arr.join().split(',').map(item => Number(item));
    
  • +',1'

    const arr = [1, [2, 3], [4, [5, [6]], 7]];
    let arrStr = arr + ',1' ; // '1,2,3,4,5,6,7,1'
    arrStr.split(',');
    let result = arrStr.split(',').map(item => Number(item));
    result.splice(-1, 1);
    
  • 手写简单的数组拍平

    const flatten = (list) => list.reduce((a, b) => a.concat(b), []) // reduce + concat
    
    const flatten = (list) => [].concat(...list) // Array.prototype.concat + 扩展运算符
    
  • 手写深层数组拍平

    function flatten(list, depth = 1) {
      if (depth === 0) return list;
      return list.reduce(
        (a, b) => a.concat(Array.isArray(b) ? flatten(b, depth - 1) : b),
        []
      ); // 判断元素是否为数组,如果是递归调用flatten处理
    

实现数组去重

1.使用 Set 对象

JavaScript 的 Set 对象是一种特殊的数据类型,它只存储唯一的值。因此,将数组转换为 Set 可以快速地去重。

let array = [1, 2, 2, 3, 4, 4, 5];  
let uniqueArray = [...new Set(array)];  
console.log(uniqueArray); // 输出:[1, 2, 3, 4, 5]

2.使用 filter 方法

也可以使用数组的 filter 方法来过滤掉重复的元素。

let array = [1, 2, 2, 3, 4, 4, 5];  
let uniqueArray = array.filter((value, index, self) => {  
    return self.indexOf(value) === index;  
});  
console.log(uniqueArray); // 输出:[1, 2, 3, 4, 5]

3.使用 reduce 方法

还可以使用 reduce 方法来实现去重。

let array = [1, 2, 2, 3, 4, 4, 5];  
let uniqueArray = array.reduce((accumulator, currentValue) => {  
    if (!accumulator.includes(currentValue)) {  
        accumulator.push(currentValue);  
    }  
    return accumulator;  
}, []);  
console.log(uniqueArray); // 输出:[1, 2, 3, 4, 5]

4.循环比较

let array = [1, 2, 2, 3, 4, 4, 5];
let uniqueArray = [];
for (let i = 0; i < array.length; i++) {
    if (!uniqueArray.includes(array)) { // or uniqueArray.indexOf(array) == -1
        uniqueArray.push(array[i]);
    }
}

Set转Array

1.Array.from

let array = Array.from(new Set([1, 2, 2, 3, 4, 4, 5]));  

2.扩展运算符

let array = [...new Set([1, 2, 2, 3, 4, 4, 5])];  

不能使用[].slice.call(),因为其只能转换类数组对象