前端面试(JavaScript)篇

73 阅读26分钟

1. 谈谈对原型链的理解

原型链是 JavaScript 中的一种机制,它用于实现继承和对象属性的查找。每一个 JavaScript 对象都有一个原型对象,对象可以通过原型对象获取到其他属性和方法。原型对象也可以有自己的原型对象,形成一个链式结构,这就是原型链。

当我们通过一个对象访问一个属性或方法时,JavaScript 引擎首先会查找该对象本身是否有该属性或方法,如果没有,就会查找该对象的原型对象,如果还没有,就会继续查找原型对象的原型对象,直到找到该属性或方法,或者找到原型链的最顶端为止。

通过原型链,我们可以实现 JavaScript 中的继承机制。如果一个对象的原型对象指向另一个对象,那么该对象就可以从另一个对象继承属性和方法。在 ES6 中,引入了 class 和 extends 语法糖,可以更方便地实现继承,但本质上还是基于原型链实现的。

需要注意的是,在修改原型对象时要小心,因为原型对象是被多个对象共享的,一旦修改可能会影响到其他对象。可以通过 Object.create() 方法创建一个新的对象并指定原型对象,避免直接修改原型对象。

2. js如何实现继承?

JavaScript中的继承可以通过以下几种方式实现:
  1. 原型链继承:通过将子类的原型对象指向父类的实例,实现继承。缺点是父类的引用类型属性会被所有子类实例共享。
function Parent() {} 
Parent.prototype.sayHello = function() { 
    console.log('Hello'); 
} 
function Child() {} 
Child.prototype = new Parent(); 
var child = new Child(); 
child.sayHello(); // 输出: Hello
  1. 构造函数继承:通过在子类构造函数中调用父类构造函数,实现继承。缺点是无法继承父类原型上的方法。
function Parent(name) { 
    this.name = name; 
    this.sayHello = function() { 
        console.log('Hello ' + this.name); 
    } 
}
function Child(name) { 
    Parent.call(this, name); 
}
var child = new Child('Tom'); 
child.sayHello(); // 输出: Hello Tom
  1. 组合继承:将原型链继承和构造函数继承结合起来,实现继承。缺点是会调用两次父类构造函数。
function Parent(name) { 
    this.name = name;
}
Parent.prototype.sayHello = function() { 
    console.log('Hello ' + this.name); 
}
function Child(name) { 
    Parent.call(this, name); 
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child = new Child('Tom'); 
child.sayHello(); // 输出: Hello Tom
  1. 寄生组合继承:通过一个空函数作为中介,避免了组合继承中调用两次父类构造函数的缺点。
function Parent(name) { 
   this.name = name;
}
Parent.prototype.sayHello = function() { 
   console.log('Hello ' + this.name); 
}
function Child(name) { 
   Parent.call(this, name); 
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

var child = new Child('Tom'); 
child.sayHello(); // 输出: Hello Tom

3. js有哪些数据类型?

JavaScript共有8种数据类型,分为2大类:

1.基本数据类型(Primitive data types):

(1)数字(Number):包括整数和浮点数,如1、1.5等。

(2)字符串(String):一串文本字符,如"Hello world"。

(3)布尔值(Boolean):只有两个值,true和false。

(4)空(Null):表示空值或不存在的对象。

(5)未定义(Undefined):表示未定义的值。

(6)符号(Symbol):表示唯一的标识符,ES6新增的数据类型。

(7)BigInt:表示任意精度的整数,可以表示非常大的整数。(BigInt 是 ES2020 新增的数据类型)

2.引用数据类型(Reference data types):

(1)对象(Object):一组无序的属性集合,每个属性都是一个键值对。

4. js有哪些判断类型的方法?

JavaScript 中常用的判断类型的方法有以下几种:

  1. typeof:可以用于判断一个值的类型,返回一个字符串,包括 "number"、"string"、"boolean"、"undefined"、"object"(包括 null)和 "function"。
  2. instanceof:可以用于判断一个对象是否是某个构造函数的实例,返回一个布尔值。
  3. Object.prototype.toString.call:可以用于获取一个值的内部类型,返回一个字符串,例如 "[object Number]"、"[object String]"、"[object Boolean]"、"[object Undefined]"、"[object Null]"、"[object Object]" 和 "[object Function]" 等。
  4. Array.isArray:可以用于判断一个值是否为数组,返回一个布尔值。

需要注意的是,typeof 对于 null 和函数类型的值都会返回 "object",而 instanceof 只能用于判断对象类型,无法判断基本数据类型。而 Object.prototype.toString.call 可以判断 null 和函数类型,但需要注意判断时需要使用 call 方法。同时,以上方法都有其局限性,需要根据具体场景选择合适的判断方法。

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

  1. Array.isArray() 方法可用于判断一个变量是否为数组,如果是数组则返回 true,否则返回 false。
  2. instanceof 运算符可用于判断一个对象是否为某个类的实例,如果 arr 是数组,则返回 true,否则返回 false。

6. Null 和 undefined 的区别?

Null 和 Undefined 都是 JavaScript 中的基本数据类型,但它们有着不同的含义。

Undefined 表示一个未定义的值,即声明了一个变量但没有给它赋值或者访问一个不存在的对象属性或方法时,返回的就是 undefined。例如:

let a;
console.log(a); // 输出 undefined

let obj = {};
console.log(obj.property); // 输出 undefined

Null 则表示一个空的对象引用,即定义了一个变量并赋值为 null,表示该变量指向一个空的对象。例如:

let b = null;
console.log(b); // 输出 null

需要注意的是,undefined 和 null 在进行相等比较时,结果为 true,但它们的类型不同,可以通过 typeof 运算符来进行区分。例如:

console.log(typeof undefined); // 输出 "undefined"
console.log(typeof null); // 输出 "object"

因此,在使用时需要注意区分它们的含义和使用场景。

7. call bind apply的区别?

call、bind 和 apply 都是 JavaScript 中用于改变函数执行上下文(即 this 指向)的方法,它们的主要区别在于传参形式和执行方式。

1. call

call 方法通过指定 this 值和参数列表来调用一个函数,它的语法如下:

function.call(thisArg, arg1, arg2, ...)

其中,thisArg 表示函数执行时的上下文对象,arg1、arg2、... 表示传递给函数的参数列表。例如:

function sayName() {
  console.log(this.name);
}

let obj = {name: 'Alice'};
sayName.call(obj); // 输出 "Alice"
2. apply

apply 方法与 call 方法类似,也是通过指定 this 值和参数列表来调用一个函数,不同的是它的参数列表是以数组的形式传递的,它的语法如下:

function.apply(thisArg, [argsArray])

其中,thisArg 表示函数执行时的上下文对象,argsArray 表示传递给函数的参数列表数组。例如:

function sayName(age) {
  console.log(this.name, age);
}

let obj = {name: 'Bob'};
sayName.apply(obj, [20]); // 输出 "Bob 20"
3. bind

bind 方法与 call 和 apply 方法不同,它并不会立即执行函数,而是返回一个新的函数,并将 this 值绑定到指定的上下文对象。它的语法如下:

function.bind(thisArg[, arg1[, arg2[, ...]]])

其中,thisArg 表示函数执行时的上下文对象,arg1、arg2、... 表示传递给函数的参数列表。例如:

function sayName(age) {
  console.log(this.name, age);
}

let obj = {name: 'Charlie'};
let sayNameWithObj = sayName.bind(obj, 30);
sayNameWithObj(); // 输出 "Charlie 30"

需要注意的是,call、apply 和 bind 方法都是函数的原型方法,因此只能用于函数的调用。并且,在使用时需要注意传递的参数类型和个数,否则会导致出错。

8. 防抖节流的概念?实现防抖和节流。

防抖和节流都是用于解决高频触发事件导致浏览器性能问题的方法。

1. 防抖

防抖的原理是在事件被触发 n 秒后再执行回调函数,如果在这 n 秒内又触发了这个事件,则会重新计时。这个方法常用于输入框输入事件的处理,例如实时搜索等。它的实现代码如下:

function debounce(fn, delay) {
  let timer = null;
  return function() {
    let context = this;
    let args = arguments;
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    timer = setTimeout(function() {
      fn.apply(context, args);
    }, delay);
  }
}

其中,fn 表示要执行的函数,delay 表示延迟的时间。

2. 节流

节流的原理是在一定时间内只执行一次回调函数,如果在这个时间内又触发了这个事件,则会忽略掉。这个方法常用于 scroll、resize 等事件的处理,例如页面滚动时实现图片懒加载等。它的实现代码如下:

function throttle(fn, delay) {
  let timer = null;
  let lastTime = 0;
  return function() {
    let context = this;
    let args = arguments;
    let nowTime = new Date().getTime();
    if (nowTime - lastTime >= delay) {
      fn.apply(context, args);
      lastTime = nowTime;
    } else {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      timer = setTimeout(function() {
        fn.apply(context, args);
        lastTime = new Date().getTime();
      }, delay - (nowTime - lastTime));
    }
  }
}

其中,fn 表示要执行的函数,delay 表示间隔的时间。

需要注意的是,防抖和节流的实现方式并不是唯一的,还可以根据具体的需求进行相应的优化。

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

深拷贝和浅拷贝都是针对 JavaScript 中对象和数组的复制操作。

1. 浅拷贝

浅拷贝只会复制对象或数组的第一层,如果对象或数组中包含了其他对象或数组,则只是复制了其引用地址,而不是复制对象或数组本身。因此,当源对象或数组中的子对象或子数组发生变化时,目标对象或数组中的对应项也会发生变化。常见的浅拷贝方式包括 Object.assign() 和扩展运算符(...)。例如:

let obj1 = {a: 1, b: {c: 2}};
let obj2 = Object.assign({}, obj1);
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 3
2. 深拷贝

深拷贝会递归地复制对象或数组及其中的所有子对象或子数组,因此复制后的对象或数组和源对象或数组是完全独立的,互不影响。常见的深拷贝方式包括 JSON.parse(JSON.stringify()) 和递归复制。例如:

let obj1 = {a: 1, b: {c: 2}};
let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 2

需要注意的是,深拷贝也有一些限制,例如无法复制函数、正则表达式等特殊的对象类型,也可能会导致循环引用的问题。

3. 实现浅拷贝和深拷贝

以下是常见的浅拷贝和深拷贝的实现方式:

浅拷贝

// Object.assign()
let obj1 = {a: 1, b: {c: 2}};
let obj2 = Object.assign({}, obj1);

// 扩展运算符(...)
let obj1 = {a: 1, b: {c: 2}};
let obj2 = {...obj1};

深拷贝

// JSON.parse(JSON.stringify())
let obj1 = {a: 1, b: {c: 2}};
let obj2 = JSON.parse(JSON.stringify(obj1));

// 递归复制
function deepClone(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  let result = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    result[key] = deepClone(obj[key]);
  }
  return result;
}
let obj1 = {a: 1, b: {c: 2}};
let obj2 = deepClone(obj1);

10. 对比一下var、const、let

在 JavaScript 中,var、const、let 都是用来声明变量的关键字,它们之间有以下几个区别:

1. 变量作用域

var 声明的变量存在函数作用域或全局作用域中,let 和 const 声明的变量存在块级作用域中。块级作用域指的是由一对花括号({})括起来的代码块,例如 if、for、while、switch 等语句块。

2. 变量声明提升

使用 var 声明的变量会发生变量声明提升,即变量可以在声明前使用,但其值为 undefined。而 let 和 const 声明的变量不存在变量声明提升,如果在声明前使用,则会抛出 ReferenceError 错误。

3. 变量重复声明

使用 var 声明的变量可以重复声明,后面的声明会覆盖前面的声明。而 let 和 const 声明的变量不允许重复声明,如果重复声明同一个变量,则会抛出 SyntaxError 错误。

4. 变量赋值和修改

使用 var 和 let 声明的变量都可以进行赋值和修改操作,而使用 const 声明的变量在声明时必须进行赋值,并且不能再修改变量的值。

综上所述,let 和 const 相比于 var 更加安全和严谨,能够避免一些常见的 JavaScript 问题。因此,一般情况下建议使用 let 和 const 来声明变量,只有在特定的情况下才使用 var。

11. 箭头函数和普通函数区别是什么?

箭头函数和普通函数的主要区别在于它们的 this 绑定和函数定义方式。
  1. this 绑定:箭头函数的 this 绑定是在定义时确定的,而普通函数的 this 绑定是在运行时确定的。箭头函数的 this 绑定指向其定义时所在的作用域中的 this 值,而普通函数的 this 绑定则是在函数被调用时根据调用方式(如函数调用、对象方法调用、构造函数调用等)动态确定的。
  2. 函数定义方式:箭头函数使用箭头(=>)来定义,而普通函数使用 function 关键字来定义。

另外,箭头函数没有自己的 arguments 对象,也不能用作构造函数。而普通函数可以使用 arguments 对象来获取所有传入的参数,并且可以通过 new 关键字来创建对象实例。

总之,箭头函数和普通函数各有优缺点,开发者应根据具体的使用场景来选择合适的函数类型。

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

使用 new 关键字创建对象的过程大致如下:
  1. 创建一个新对象。
  2. 将这个新对象的原型(__proto__)指向构造函数的原型对象(prototype)。
  3. 将构造函数的 this 指向这个新对象。
  4. 执行构造函数中的代码,将属性和方法添加到新对象中。
  5. 如果构造函数返回一个对象,则返回该对象;否则返回新对象。

具体来说,使用 new 关键字创建对象时,会先创建一个空对象,然后将该对象的原型指向构造函数的原型对象,这样就可以访问到构造函数原型对象中定义的属性和方法。接着,将构造函数中的 this 指向这个新对象,并执行构造函数中的代码,将属性和方法添加到新对象中。最后,如果构造函数返回一个对象,则直接返回该对象;否则返回新对象

示例代码如下:

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

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

const person = new Person('Alice', 20);
person.sayHello(); // 输出:Hello, my name is Alice.

在这个例子中,使用 new 关键字创建了一个 Person 对象,构造函数中的代码将 name 和 age 属性添加到了新对象中,而 sayHello 方法则添加到了新对象的原型对象中。最终,通过调用 sayHello 方法,可以输出新对象中的 name 属性。

13. this指向系列问题

this 指向是 JavaScript 中一个非常重要的概念,它可以影响函数的执行结果。

1. this 的默认指向是什么?

在普通函数中,this 的默认指向是全局对象(在浏览器中是 window 对象,在 Node.js 中是 global 对象)。在严格模式下,函数的 this 默认为 undefined。

2. 如何改变 this 的指向?

可以使用 callapplybind 方法来改变函数的 this 指向。这些方法都是 Function 对象的原型方法,它们接受一个参数作为 this 指向的对象,其中 callapply 方法可以接受多个参数,而 bind 方法只接受一个参数,并返回一个新的函数。

3. 箭头函数中的 this 指向是什么?

箭头函数中的 this 指向是定义时所在的作用域中的 this 值,而不是运行时的 this 值。因此,在箭头函数中无法通过 callapplybind 方法来改变 this 指向。

4. 在事件处理函数中,this 指向是什么?

在 DOM 事件处理函数中,this 指向触发事件的 DOM 元素。

5. 在对象方法中,this 指向是什么?

在对象方法中,this 指向调用该方法的对象。

6. 在构造函数中,this 指向是什么?

在构造函数中,this 指向正在创建的对象实例。

14. 手写bind方法

实现 bind 方法的主要思路是创建一个新的函数,并在该函数内部调用原函数,并将原函数的 this 指向指定的对象。以下是一个简单的手写 bind 方法的实现:

Function.prototype.myBind = function(context) {
  var fn = this; // 保存原函数
  return function() {
    fn.apply(context, arguments); // 调用原函数并改变 this 指向
  }
}

该实现中,首先保存原函数的引用,然后返回一个新的函数。在新函数内部,调用原函数,并将其 this 指向指定的 context 对象。通过 apply 方法,将原函数的参数传递到新函数中,从而完成了参数的传递。

以下是一个使用该实现的示例:

var obj = {
  name: 'Alice'
};

function sayHello() {
  console.log(`Hello, ${this.name}!`);
}

var hello = sayHello.myBind(obj);

hello(); // 输出:Hello, Alice!

在上面的示例中,首先定义了一个对象 obj,然后定义了一个函数 sayHello,该函数输出一个字符串,并使用 this 访问了对象的 name 属性。然后,使用 myBind 方法将 sayHello 函数绑定到 obj 对象上,并返回一个新的函数 hello。最后,调用新函数 hello,输出了正确的字符串。

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

闭包是指在一个函数内部定义的函数,该内部函数可以访问到外部函数的局部变量和参数,并且可以在外部函数调用结束后,仍然保持对这些变量的引用,从而形成了一个“闭合的”作用域。简单来说,闭包就是一个函数可以访问它父级作用域中的变量。

应用场景

闭包的应用场景很多,比如:

  1. 封装变量:通过闭包可以封装变量,使其不受外部干扰,从而实现数据的私有化。
  2. 模块化开发:通过闭包可以实现模块化开发,将一些变量和方法封装在闭包内部,从而防止全局变量污染和命名冲突。
  3. 延迟执行:通过闭包可以实现一些延迟执行的操作,比如定时器、事件监听器等。
  4. 缓存数据:通过闭包可以实现一些数据的缓存,避免重复计算。
缺点

闭包的缺点是可能会导致内存泄漏。由于闭包会保持对外部变量的引用,如果闭包函数没有及时释放,那么这些变量就无法被垃圾回收器回收,从而导致内存泄漏。为了避免这种情况,可以手动将闭包函数置为 null,或者使用 let 或 const 关键字来声明变量,从而使其在块级作用域结束后自动释放。

另外,在使用闭包时还需要注意一些问题,比如在循环中使用闭包会导致变量共享和值不正确的问题,可以通过使用立即执行函数来解决。另外,过多的使用闭包也会导致代码可读性降低和性能问题,因此需要适度使用。

16. 谈谈对js事件循环的理解?

JavaScript 是单线程执行的语言,这意味着在任何时候,JavaScript 只能执行一个任务。因此,需要一种机制来协调多个任务的执行,这就是事件循环机制。

事件循环是 JavaScript 引擎中的一个机制,它用于协调多个任务的执行顺序。当一个任务执行完毕后,它会将下一个任务放入任务队列中,等待 JavaScript 引擎执行。JavaScript 引擎会不断地从任务队列中取出任务并执行,直到任务队列为空。

事件循环中,任务可以分为两种类型:宏任务微任务

宏任务包括整体代码(script),setTimeout,setInterval,I/O 操作等。当所有的微任务执行完毕后,JavaScript 引擎会从宏任务队列中取出一个任务执行,直到宏任务队列为空。

微任务包括 Promise.then,MutationObserver,process.nextTick 等。当一个宏任务执行完毕后,在 JavaScript 引擎执行下一个宏任务之前,会将所有微任务执行完毕。

事件循环的机制保证了 JavaScript 单线程的特性,同时也支持异步编程。在编写异步代码时,可以利用事件循环机制来协调多个异步任务的执行顺序,从而实现更复杂的功能。

17. 谈谈对promise理解?

Promise 是 JavaScript 中一种用于异步编程的解决方案,它可以优雅地处理异步代码的回调地狱问题,使代码更加清晰、易读、易维护。

Promise 有三种状态:pending(等待中)、fulfilled(已成功)和rejected(已失败)。当一个 Promise 被创建时,它处于等待状态,可以通过调用 resolve 函数将其状态改变为已成功,或者通过调用 reject 函数将其状态改变为已失败

Promise 可以使用 then 方法添加回调函数,当 Promise 状态改变时,then 方法会被触发,执行对应的回调函数。then 方法可以接收两个参数,第一个参数是 Promise 成功时的回调函数,第二个参数是 Promise 失败时的回调函数。

Promise 还可以通过链式调用 then 方法,实现多个异步操作的顺序执行。在链式调用中,每个 then 方法都会返回一个新的 Promise 对象,因此可以在 then 方法中继续添加回调函数。

Promise 还提供了 catch 方法,用于捕获 Promise 状态改变时产生的错误。catch 方法可以链式调用,用于捕获前面所有 then 方法中产生的错误。

Promise 的优点在于它解决了回调地狱问题,使异步代码更加清晰、易读、易维护。同时,Promise 也支持链式调用,可以使多个异步操作按照顺序执行。另外,Promise 还提供了一些方法,比如 all、race、resolve、reject 等,用于更加灵活地处理异步操作。

Promise 也存在一些缺点。Promise 无法取消,一旦创建就无法中途取消。另外,Promise 无法直接处理异步代码中的异常,需要使用 try-catch 或者 catch 方法来捕获错误。

18. 手写 Promise

class Promise {
  constructor(executor) {
    this.state = 'pending'
    this.value = null
    this.reason = null
    this.onResolvedCallbacks = []
    this.onRejectedCallbacks = []
    let resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled'
        this.value = value
        this.onResolvedCallbacks.forEach(fn => fn())
      }
    }
    let reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected'
        this.reason = reason
        this.onRejectedCallbacks.forEach(fn => fn())
      }
    }
    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }
  then(onFulfilled, onRejected) {
    if (this.state === 'fulfilled') {
      onFulfilled(this.value)
    }
    if (this.state === 'rejected') {
      onRejected(this.reason)
    }
    if (this.state === 'pending') {
      this.onResolvedCallbacks.push(() => {
        onFulfilled(this.value)
      })
      this.onRejectedCallbacks.push(() => {
        onRejected(this.reason)
      })
    }
  }
}

19. 实现 Promise.all方法

Promise.all方法用于等待多个Promise对象的结果,并在所有Promise对象都完成时返回一个包含所有结果的数组。

下面是一种实现Promise.all方法的方式:

function all(promises) {
  return new Promise(function(resolve, reject) {
    var results = [];
    var count = promises.length;
    promises.forEach(function(promise, index) {
      promise.then(function(result) {
        results[index] = result;
        count--;
        if (count === 0) {
          resolve(results);
        }
      }).catch(function(error) {
        reject(error);
      });
    });
  });
}

这个实现中,首先创建一个新的Promise对象,然后遍历传入的Promise数组,并在每个Promise对象完成后将结果存储到结果数组中。当所有Promise对象都完成后,通过调用resolve方法返回结果数组。如果任何一个Promise对象失败,则通过调用reject方法返回错误信息。

20. Typescript中type和interface的区别是什么?

在 Typescript 中,type 和 interface 都可以用来定义对象的结构类型,但它们有以下区别:

  1. type 可以定义基本类型、联合类型、元组类型、字面量类型、交叉类型、函数类型等,而 interface 只能定义对象类型。
  2. type 可以使用 typeof 操作符获取变量的类型,而 interface 不能。
  3. type 可以使用 extends 关键字来定义一个类型是另一个类型的扩展,而 interface 不支持这个特性。
  4. 当定义同名的 type 和 interface 时,type 会覆盖 interface 的定义。

因为 type 声明可以定义联合类型和交叉类型等高级类型,所以在定义复杂类型时,使用 type 声明会更方便。而在定义对象类型时,使用 interface 声明更符合语义上的含义。

21. 讲讲Typescript中的泛型?

Typescript 中的泛型(Generics)是一种可以在代码层面实现类型参数化的方法。它可以让我们在定义函数、类、接口等时,将类型作为参数传入,从而更灵活地处理不同类型的数据。

泛型的使用方法是在类型或函数名后加上尖括号(<>),并在其中定义一个类型参数。例如:

function identity<T>(arg: T): T {
  return arg;
}

在上面的例子中,我们定义了一个泛型函数 identity,它接受一个类型为 T 的参数 arg,并返回一个类型为 T 的值。这里的 T 就是一个类型参数,可以在函数的调用时确定具体的类型。例如:

let output = identity<string>("hello world");
console.log(output); // 输出 "hello world"

在调用 identity 函数时,我们可以将类型参数设置为 string,这样函数就会返回一个 string 类型的值。

除了函数,泛型还可以应用于类和接口的定义。例如,我们可以定义一个泛型类 DataHolder,用来存储不同类型的数据:

class DataHolder<T> {
  private data: T;
  constructor(data: T) {
    this.data = data;
  }
  getData(): T {
    return this.data;
  }
}

let stringHolder = new DataHolder<string>("hello");
console.log(stringHolder.getData()); // 输出 "hello"

let numberHolder = new DataHolder<number>(42);
console.log(numberHolder.getData()); // 输出 42

在上面的例子中,我们定义了一个泛型类 DataHolder,它接受一个类型参数 T,并用它来定义成员变量 data 的类型和方法 getData 的返回类型。在创建类的实例时,我们可以把类型参数设置为具体的类型,从而创建出不同类型的实例。

22. Typescript如何实现一个函数的重载?

在 Typescript 中,我们可以使用函数重载(Function Overloading)来定义一组函数,这些函数在参数类型或个数不同的情况下,可以有不同的实现。

函数重载的语法如下:

function fnName(p1: type1): returnType1;
function fnName(p1: type2, p2: type3): returnType2;
function fnName(p1: type4, p2: type5, p3: type6): returnType3;
// 更多重载...

其中,每一行都是一个函数的定义,它们使用相同的函数名,但参数类型或个数不同,返回值类型也可以不同。当我们调用这个函数时,Typescript 会根据参数的类型和个数,自动匹配到对应的函数定义,并执行对应的实现。

例如,我们可以定义一个函数 add,它可以接受两个参数(任意类型的数字),并返回它们的和:

function add(x: number, y: number): number {
  return x + y;
}

现在,我们想让这个函数也能接受字符串类型的数字作为参数,我们可以使用函数重载:

function add(x: number, y: number): number;
function add(x: string, y: string): number;
function add(x: any, y: any): number {
  return parseInt(x) + parseInt(y);
}

在上面的例子中,我们定义了三个函数重载,第一个重载接受两个 number 类型的参数,返回一个 number 类型的值;第二个重载接受两个 string 类型的参数,也返回一个 number 类型的值;第三个重载是一个实现函数,它接受任意类型的参数,并将参数转换为数字后相加,并返回一个 number 类型的值。

现在,我们可以调用这个函数,传入不同类型的参数,Typescript 会自动匹配到对应的重载,并执行对应的实现:

console.log(add(1, 2)); // 输出 3
console.log(add("3", "4")); // 输出 7
console.log(add("1", 2)); // 报错,没有匹配到对应的重载

23. CmmonJS和ESM区别?

CommonJS和ESM(ES6模块)是两种不同的模块化规范。

CommonJS是一种用于JavaScript的模块化规范,它允许在运行时动态加载模块。它的主要目标是在服务器端使用,但它也可以在浏览器端使用。在Node.js中,它是默认的模块化规范。

ESM是ES6中引入的一种模块化规范,它是一种静态加载模块的机制,可以在编译时确定模块之间的依赖关系。它支持导入和导出语法,并且可以在浏览器中使用,但需要使用特定的构建工具进行转换。

区别:

  1. CommonJS是动态加载,ESM是静态加载。
  2. CommonJS是同步加载,ESM是异步加载。
  3. CommonJS只支持exports和module.exports语法,ESM支持import和export语法。
  4. CommonJS是运行时加载,ESM是编译时加载。
  5. CommonJS在服务器端得到广泛应用,ESM在浏览器端得到广泛应用。

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

柯里化(Currying)是一种函数式编程技术,它是把接受多个参数的函数转换为接受一个单一参数的函数,并返回一个新的函数,这个新函数负责接受余下的参数并返回结果。柯里化可以帮助我们更好地组织和复用代码

柯里化的作用
  1. 参数复用:通过柯里化函数,可以把一些常用的参数提前传入,让函数更加灵活和易用。
  2. 延迟计算:通过柯里化函数,可以把一些参数缓存起来,等到后面需要的时候再计算,提高程序的性能。
  3. 参数推导:通过柯里化函数,可以推导出一个新的函数,这个函数只需要传入一部分参数,就可以得到一个新的函数,这个函数接受剩余的参数。
实现柯里化的方法
  1. 手写柯里化函数:
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...args2) {
        return curried.apply(this, args.concat(args2));
      }
    }
  }
}
  1. 使用第三方库,如lodash的curry函数:
const { curry } = require('lodash');

function add(a, b, c) {
  return a + b + c;
}

const curriedAdd = curry(add);

curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6

以上两种方法中,第一种方法可以手动实现柯里化,更加灵活,但是需要写更多的代码;第二种方法使用第三方库,代码更加简洁,但是需要引入额外的库。

25. 讲讲js垃圾回收机制

JavaScript垃圾回收(Garbage Collection)是指在JavaScript运行时自动管理内存的过程,它的主要任务是自动回收不再使用的对象,释放内存空间,防止内存泄漏。

JavaScript中的垃圾回收机制主要有两种:
  1. 标记清除(Mark-and-Sweep):垃圾收集器会在内存中找到所有的对象,并标记这些对象。然后,垃圾收集器会从根元素开始遍历所有的对象,并标记它们的引用对象。最后,垃圾收集器会清除所有没有被标记的对象,释放它们所占用的内存空间。
  2. 引用计数(Reference Counting):垃圾收集器会记录每个对象被引用的次数。当一个对象的引用计数为0时,垃圾收集器就会把它回收掉,并释放它所占用的内存空间。

在实际应用中,标记清除(Mark-and-Sweep)是JavaScript中最常用的垃圾回收机制。它可以在运行时自动回收不再使用的对象,避免内存泄漏。

JavaScript中的垃圾回收机制是自动的,不需要开发者手动管理内存空间。开发者只需要避免在代码中出现内存泄漏的情况,比如循环引用、全局变量等,就能避免JavaScript中的内存泄漏问题。

26. 实现一个发布订阅

发布订阅模式是一种经典的设计模式,它用于解耦应用程序中的不同组件。在该模式中,应用程序中的一个组件(发布者)发布消息,另一个组件(订阅者)订阅消息并在消息发布时执行相应的操作。下面是一个简单的发布订阅模式的实现:

class EventEmitter {
  constructor() {
    this.events = {};
  }
  
  // 订阅事件
  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }
  
  // 发布事件
  emit(eventName, ...args) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(callback => {
        callback(...args);
      });
    }
  }
  
  // 取消订阅
  off(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
    }
  }
}

使用示例:

const emitter = new EventEmitter();

// 订阅事件
emitter.on('hello', name => {
  console.log(`Hello, ${name}!`);
});

// 发布事件
emitter.emit('hello', 'world'); // 输出 "Hello, world!"

// 取消订阅
const callback = name => {
  console.log(`Goodbye, ${name}!`);
};
emitter.on('goodbye', callback);
emitter.emit('goodbye', 'world'); // 输出 "Goodbye, world!"
emitter.off('goodbye', callback);
emitter.emit('goodbye', 'world'); // 不输出任何内容

在上面的示例中,我们定义了一个EventEmitter类,它包含三个方法:on、emit和off。on方法用于订阅事件,emit方法用于发布事件,off方法用于取消订阅。我们可以使用这些方法来实现发布订阅模式。

27. 如何实现数组拍平?

数组拍平是指将一个多维数组转化为一维数组的操作。下面是几种实现数组拍平的方法:

1. 使用递归方法实现数组拍平:
function flatten(arr) {
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      result = result.concat(flatten(arr[i]));
    } else {
      result.push(arr[i]);
    }
  }
  return result;
}
2. 使用reduce方法实现数组拍平:
function flatten(arr) {
  return arr.reduce((result, item) => {
    if (Array.isArray(item)) {
      result = result.concat(flatten(item));
    } else {
      result.push(item);
    }
    return result;
  }, []);
}
3. 使用Generator函数实现数组拍平:
function* flatten(arr) {
  for (let i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      yield* flatten(arr[i]);
    } else {
      yield arr[i];
    }
  }
}

const arr = [1, 2, [3, 4, [5, 6]]];
const result = [...flatten(arr)];
console.log(result); // [1, 2, 3, 4, 5, 6]
4. 使用ES6的扩展运算符实现数组拍平:
const arr = [1, 2, [3, 4, [5, 6]]];
const result = [].concat(...arr);
console.log(result); // [1, 2, 3, 4, 5, 6]

28. 如何实现数组去重?

数组去重是指将数组中重复的元素去除,只保留一个。下面是几种实现数组去重的方法:

1. 使用Set数据结构实现数组去重:
const arr = [1, 2, 3, 2, 1];
const result = [...new Set(arr)];
console.log(result); // [1, 2, 3]
2. 使用for循环和indexOf方法实现数组去重:
function unique(arr) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    if (result.indexOf(arr[i]) === -1) {
      result.push(arr[i]);
    }
  }
  return result;
}

const arr = [1, 2, 3, 2, 1];
const result = unique(arr);
console.log(result); // [1, 2, 3]
3. 使用for循环和includes方法实现数组去重:
function unique(arr) {
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    if (!result.includes(arr[i])) {
      result.push(arr[i]);
    }
  }
  return result;
}

const arr = [1, 2, 3, 2, 1];
const result = unique(arr);
console.log(result); // [1, 2, 3]
4. 使用reduce方法实现数组去重:
function unique(arr) {
  return arr.reduce((result, item) => {
    if (!result.includes(item)) {
      result.push(item);
    }
    return result;
  }, []);
}

const arr = [1, 2, 3, 2, 1];
const result = unique(arr);
console.log(result); // [1, 2, 3]

以上是几种常见的实现数组去重的方法。其中使用Set数据结构实现的方法是最简洁、最高效的方法。