当前位置:   article > 正文

前端总结——JavaScript_es next新特性有哪些?

es next新特性有哪些?

文章目录


谈谈对JS的理解。

JavaScript(简称JS)是一种脚本语言,通常用于在网页上实现交互效果、动态内容和用户体验的改善。下面是对JavaScript的一些常见理解:

客户端脚本语言:JavaScript主要运行在客户端(浏览器)上,通过浏览器解释执行。它能够直接与用户的网页进行交互,动态地修改和更新网页的内容、样式和行为。

强大的功能:JavaScript具备广泛的功能,可以操作DOM(文档对象模型)来改变网页结构,处理表单验证和提交,执行动画效果,发送异步请求(AJAX),处理数据等。它还可以与HTML和CSS无缝集成,实现复杂的网页交互和应用程序开发。

跨平台性:JavaScript不仅可以在浏览器中运行,还可以在其他环境中执行,比如服务器端(使用Node.js),移动应用开发(使用React Native、Ionic等框架)等。这种跨平台性使得JavaScript成为一种广泛使用的通用编程语言。

动态类型:JavaScript是一种动态类型语言,不需要显式地声明变量的类型。变量的类型会根据赋值的值自动确定,并且可以在运行时动态改变变量的类型。

函数式编程支持:JavaScript是一种多范式的语言,支持面向对象编程(OOP)和函数式编程(FP)。它具备一些函数式编程的特性,如高阶函数、匿名函数、闭包等,使得编写具有抽象和复用性的代码更加灵活和方便。

生态系统和社区支持:JavaScript拥有庞大的生态系统和活跃的社区。有大量的开源库、框架和工具可供使用,可以加快开发速度,并提供解决各种开发任务的解决方案。同时,社区提供了丰富的文档、教程和支持,有助于学习和提高开发技能。

不断发展:JavaScript在不断发展和演进中,新的标准(如ES6、ES7等)不断推出,引入了新的语法、特性和改进。这使得JavaScript变得更加强大、高效和易用。

总之,JavaScript是一种灵活、功能丰富且广泛应用的脚本语言。它的优势在于与网页紧密集成、跨平台性、丰富的生态系统和社区支持。通过JavaScript,开发者可以创建交互式、动态和高效的网页应用程序。

介绍js的事件机制

JavaScript的事件机制是一种用于处理用户交互和其他异步操作的机制。它允许开发者定义事件处理程序,以响应特定事件的触发。

事件机制的主要组成部分包括以下几个要素:

  1. 事件:事件是在特定情况下触发的动作或状态改变,例如用户单击按钮、页面加载完成、键盘按键按下等。
  2. 事件触发器:事件触发器是导致事件发生的实体,可以是用户的交互行为(例如鼠标点击)或其他程序触发的动作。
  3. 事件处理程序:事件处理程序是在事件触发时执行的代码块,用于定义事件发生后要执行的操作。开发者可以编写事件处理程序来响应特定的事件。
  4. 事件监听器:事件监听器是一种机制,用于注册事件处理程序并将其与特定事件关联起来。通过事件监听器,可以指定在特定事件触发时要执行的事件处理程序。
  5. 事件队列:事件队列是一个存储待处理事件的队列结构。当事件触发时,相关的事件信息被添加到事件队列中,并按照先进先出(FIFO)的顺序进行处理。

基本的事件处理流程如下:

  1. 注册事件监听器:通过使用特定的API,开发者可以将事件监听器注册到特定的事件上,指定要执行的事件处理程序。
  2. 事件触发:当事件触发条件满足时,事件触发器会将事件信息添加到事件队列中。
  3. 事件处理:事件处理器从事件队列中获取事件信息,并执行相应的事件处理程序。处理程序可以访问事件对象,以获取与事件相关的信息。
  4. 完成处理:事件处理程序执行完成后,可以进行必要的清理操作,并继续等待下一个事件的触发。

JavaScript中的事件机制使开发者能够通过编写事件处理程序来响应用户交互和其他异步操作,从而实现动态和交互式的网页应用程序。常见的事件包括点击事件、鼠标移动事件、键盘事件、表单提交事件、页面加载事件等。通过合理地利用事件机制,可以增强用户体验,并实现丰富的交互功能。

1.谈谈对原型链的理解。

  原型链是JavaScript中用于实现对象之间继承关系的机制。在JavaScript中,每个对象都有一个内部属性[[Prototype]](也可以通过 __proto__ 属性来访问),它指向该对象的原型对象。

  当我们访问一个对象的属性或方法时,如果该对象自身没有定义该属性或方法,JavaScript会沿着原型链向上查找,直到找到该属性或方法为止。这个查找过程就是原型链的核心概念。

  每个原型对象也有自己的原型,所以形成了一个原型链。原型链的顶端是Object.prototype,它是所有对象的基础原型。Object.prototype 自身没有原型,因此原型链的最后一环是 null

  通过原型链,对象可以继承其原型对象的属性和方法。当我们访问一个对象的属性时,JavaScript会先在对象自身的属性中查找,如果找不到,它会继续在对象的原型对象中查找。如果还找不到,就会继续在原型对象的原型对象中查找,依次类推,直到找到属性或到达原型链的末尾。

继承的好处是可以共享代码和行为。例如,我们可以创建一个 Person 构造函数,并将其原型对象中的方法(如 sayHello)定义为所有 Person 实例共享的方法。然后,我们可以创建多个 Person 对象,并且它们都可以访问和使用这个共享的方法。
以下是一个简单的示例来说明原型链的概念:

// 构造函数
function Person(name) {
  this.name = name;
}

// 在原型对象中定义方法
Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};

// 创建对象实例
var person1 = new Person('Alice');
var person2 = new Person('Bob');

// 调用共享的方法
person1.sayHello(); // 输出: Hello, my name is Alice
person2.sayHello(); // 输出: Hello, my name is Bob

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

  在上述示例中,Person.prototypeperson1 person2 的原型对象。它包含了共享的方法 sayHello。当我们调用person1.sayHello()person2.sayHello() 时,JavaScript会在 person1person2对象自身找不到 sayHello 方法,然后沿着原型链去 Person.prototype 中查找并执行该方法。

  这就是原型链的基本概念和工作原理。通过原型链,JavaScript实现了对象之间的继承关系,允许对象共享属性和方法,以及实现面向对象编程的特性。

2.js如何实现继承?

  1. 原型链继承:
      使用原型链继承的方式是通过让一个构造函数的原型对象指向另一个构造函数的实例,从而实现继承。这样子类的实例就可以访问到父类原型对象中定义的属性和方法。
// 父类构造函数
function Parent(name) {
  this.name = name;
}

// 父类原型方法
Parent.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};

// 子类构造函数
function Child(name, age) {
  Parent.call(this, name); // 调用父类构造函数,继承父类的实例属性
  this.age = age;
}

// 将子类的原型对象指向父类的实例
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 修复子类原型的 constructor 属性

// 子类原型方法
Child.prototype.sayAge = function() {
  console.log('I am ' + this.age + ' years old');
};

// 创建子类实例
var child = new Child('Alice', 20);

child.sayHello(); // 输出: Hello, my name is Alice
child.sayAge(); // 输出: I am 20 years old
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

  在上述示例中,Child.prototype 的原型对象被设置为 Object.create(Parent.prototype),这样子类的原型对象就继承了父类的原型对象。然后,我们可以在子类的原型对象上定义自己的方法,实现子类特有的行为。
2.构造函数继承(借用构造函数):
  使用构造函数继承的方式是在子类构造函数中通过 callapplybind 方法调用父类构造函数,从而继承父类的实例属性。

// 父类构造函数
function Parent(name) {
  this.name = name;
}

// 子类构造函数
function Child(name, age) {
  Parent.call(this, name); // 调用父类构造函数,继承父类的实例属性
  this.age = age;
}

// 创建子类实例
var child = new Child('Alice', 20);

console.log(child.name); // 输出: Alice
console.log(child.age); // 输出: 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

  在上述示例中,通过 Parent.call(this, name) 将父类的构造函数应用于子类实例,从而继承了父类的实例属性。这种方式只能继承实例属性,无法继承父类原型对象中定义的方法。

3.组合继承(Combination Inheritance):
  使用原型继承和构造函数继承的组合方式实现继承关系。通过调用父对象的构造函数来继承属性,然后通过将子对象的原型对象指向父对象的实例来继承方法。

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

Parent.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};

function Child(name) {
  Parent.call(this, name);
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child = new Child('Alice');
child.sayHello(); // 输出: Hello, my name is Alice
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

3.js有哪些数据类型?

  1. 基本数据类型(Primitive Types):
  • 数字(Number): 表示数值,包括整数和浮点数。
  • 字符串(String): 表示文本数据,用于存储和操作文本。
  • 布尔值(Boolean): 表示逻辑值,只有两个取值:true和false。
  • undefined: 表示未定义的值,当变量声明但未赋值时默认为undefined。
  • null: 表示空值或者空对象引用。
  • Symbol (ES6新增): 表示唯一的标识符,用于创建对象的属性键。
  1. 引用数据类型(Reference Types):
  • 对象(Object): 表示复杂数据类型,可以包含多个属性和方法。
  • 数组(Array): 表示一组有序的值的集合,可以通过索引访问和操作其中的元素。
  • 函数(Function): 表示可执行的代码块,可以被调用和传递参数。
  • 正则表达式(RegExp): 表示字符串匹配的模式,用于在文本中进行模式匹配和替换。
  • 日期(Date): 表示日期和时间的数据类型,用于处理日期和时间相关操作。
  1. 除了以上列举的数据类型,JavaScript还有一些特殊类型和特殊值,例如:
  • NaN: 表示一个非数值(Not-a-Number)的特殊值,通常由非法的数学运算导致。
  • Infinity: 表示正无穷大的特殊值,用于表示超出 JavaScript 数值范围的数值。
  • -Infinity: 表示负无穷大的特殊值,用于表示超出 JavaScript 数值范围的负数值。

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

  1. typeof 操作符:

  使用typeof操作符可以判断一个值的基本类型。
示例代码:

typeof 42; // "number"
typeof "Hello"; // "string"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof null; // "object"(注意,null的类型被判断为"object",这是一个历史遗留问题)
typeof Symbol("symbol"); // "symbol"(ES6新增的Symbol类型)
typeof { name: "Alice" }; // "object"
typeof [1, 2, 3]; // "object"
typeof function() {}; // "function"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  1. instanceof 操作符:

  使用instanceof操作符可以判断一个对象是否属于某个构造函数的实例。

var arr = [1, 2, 3];
arr instanceof Array; // true

function Person(name) {
  this.name = name;
}
var person = new Person("Alice");
person instanceof Person; // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  1. Object.prototype.toString() 方法:

  使用Object.prototype.toString()方法可以获取一个对象的内部属性[[Class]],从而判断对象的类型。

Object.prototype.toString.call(42); // "[object Number]"
Object.prototype.toString.call("Hello"); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(Symbol("symbol")); // "[object Symbol]"
Object.prototype.toString.call({ name: "Alice" }); // "[object Object]"
Object.prototype.toString.call([1, 2, 3]); // "[object Array]"
Object.prototype.toString.call(function() {}); // "[object Function]"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  1. Array.isArray() 方法:

  用Array.isArray()方法可以判断一个值是否为数组类型。

Array.isArray([1, 2, 3]); // true
Array.isArray({ name: "Alice" }); // false
  • 1
  • 2

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

  1. Array.isArray() 方法:

  使用Array.isArray()方法可以判断一个变量是否为数组类型。

var arr = [1, 2, 3];
Array.isArray(arr); // true

var obj = { name: "Alice" };
Array.isArray(obj); // false
  • 1
  • 2
  • 3
  • 4
  • 5
  1. instanceof 操作符:

  使用instanceof操作符可以判断一个变量是否为Array构造函数的实例。

var arr = [1, 2, 3];
arr instanceof Array; // true

var obj = { name: "Alice" };
obj instanceof Array; // false
  • 1
  • 2
  • 3
  • 4
  • 5

3.Object.prototype.toString() 方法:

  使用Object.prototype.toString()方法可以获取一个变量的内部属性[[Class]],从而判断变量的类型。

var arr = [1, 2, 3];
Object.prototype.toString.call(arr) === "[object Array]"; // true

var obj = { name: "Alice" };
Object.prototype.toString.call(obj) === "[object Array]"; // false
  • 1
  • 2
  • 3
  • 4
  • 5

注意,以上方法中,Array.isArray() 是最推荐使用的方法,因为它是专门用来判断一个变量是否为数组的标准方法,而不会受到原型链的干扰。其他方法可能在某些特殊情况下产生误判,或者在跨窗口/跨帧的情况下失效。因此,对于数组判断,建议使用 Array.isArray() 方法。

6. Null 和 undefined 的区别?

  在JavaScript中,null和undefined都表示没有值的情况,但它们在使用和含义上有一些区别:

  1. undefined:
  • undefined表示一个变量已经被声明,但尚未赋予任何值。
  • 当变量声明但未初始化时,默认值为undefined。
  • 也可以将变量显式赋值为undefined。
var x;
console.log(x); // 输出: undefined

var y = undefined;
console.log(y); // 输出: undefined
  • 1
  • 2
  • 3
  • 4
  • 5
  1. null:
  • null表示一个变量被赋予了一个空的、不存在的或无效的值。
  • null是一个表示"空"的特殊值,表示变量不指向任何对象。
  • null需要显式赋值为null。
var z = null;
console.log(z); // 输出: null
  • 1
  • 2

区别:

  • undefined是一个表示"未定义"的原始值,表示变量已声明但没有赋值。null是一个表示"空"的特殊值,表示变量被赋予了一个空的、不存在的或无效的值。
  • undefined是一个数据类型,而null是一个对象类型(在技术上,null是一个空对象指针)。
  • 在变量未赋值的情况下,默认的初始值是undefined。而null必须显式地赋值给变量。
  • 在使用typeof操作符检测时,undefined会返回"undefined",而null会返回"object"。这是JavaScript语言本身的设计缺陷。

7. call bind apply的区别?

  在JavaScript中,call、bind和apply都是用于调用函数的方法,它们有一些区别和不同的使用方式:

  1. call:
  • call方法是函数对象的一个方法,用于调用一个函数并指定函数内部的this指向。
  • call方法接受一个指定的this值和一系列的参数。
function greet(name) {
  console.log('Hello, ' + name);
}

greet.call(null, 'Alice'); // 输出: Hello, Alice
  • 1
  • 2
  • 3
  • 4
  • 5
  1. apply:
  • apply方法和call方法类似,也是用于调用一个函数并指定函数内部的this指向。
  • apply方法接受一个指定的this值和一个参数数组(或类数组对象)。
function greet(name) {
  console.log('Hello, ' + name);
}

greet.apply(null, ['Alice']); // 输出: Hello, Alice
  • 1
  • 2
  • 3
  • 4
  • 5
  1. bind:
  • bind方法用于创建一个新函数,并将原函数中的this指向绑定到指定的对象上。
  • bind方法返回一个新函数,可以稍后调用。
function greet(name) {
  console.log('Hello, ' + name);
}

var greetFunc = greet.bind(null, 'Alice');
greetFunc(); // 输出: Hello, Alice
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

主要区别和用法总结如下:

  • callapply都是直接调用函数并指定this值,只是传递参数的方式不同:call是逐个传参,apply是传递参数数组。
  • bind方法不会立即调用函数,而是返回一个新函数,可以稍后调用。它将函数和指定的this值绑定在一起,方便稍后再次调用。
  • 所有这些方法都用于改变函数内部的this指向,使函数在不同的上下文中执行。

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

  防抖(Debounce)和节流(Throttle)是常用的优化技术,用于控制函数在频繁触发时的执行次数,以提高性能和减少资源消耗。

  1. 防抖(Debounce):
  • 防抖的目标是在函数连续触发的情况下,只执行最后一次触发的函数调用。
  • 当一个函数被触发后,在指定的延迟时间内如果再次触发该函数,则重新计时,直到延迟时间内没有再次触发,才执行该函数。
  • 这种方式适用于需要等待用户停止操作后再执行的场景,比如输入框的搜索建议、窗口大小调整的响应等。
function debounce(func, delay) {
  let timerId;
  return function (...args) {
    clearTimeout(timerId);
    timerId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// 示例使用
function handleInput() {
  console.log('Input debounced');
}

const debouncedHandleInput = debounce(handleInput, 300);

// 模拟输入框输入
debouncedHandleInput(); // 不会立即执行
debouncedHandleInput(); // 不会立即执行
debouncedHandleInput(); // 300毫秒后执行
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  1. 节流(Throttle):
  • 节流的目标是在函数连续触发的情况下,限制函数的执行频率。
  • 当一个函数被触发后,在指定的时间间隔内只执行一次函数调用。
  • 这种方式适用于需要限制函数执行频率的场景,比如滚动事件、按钮点击等。
function throttle(func, interval) {
  let lastTime = 0;
  return function (...args) {
    const currentTime = Date.now();
    if (currentTime - lastTime >= interval) {
      func.apply(this, args);
      lastTime = currentTime;
    }
  };
}

// 示例使用
function handleScroll() {
  console.log('Scroll throttled');
}

const throttledHandleScroll = throttle(handleScroll, 200);

// 模拟滚动事件
throttledHandleScroll(); // 第一次立即执行
throttledHandleScroll(); // 在200毫秒内不会执行
throttledHandleScroll(); // 在200毫秒内不会执行
// 200毫秒后再次执行
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

防抖和节流的关系:

  • 防抖和节流都可以用来限制函数的执行频率,避免过于频繁的调用,减少资源消耗。
  • 防抖和节流都使用定时器来延迟函数的执行,但它们的触发时机和执行方式不同。

防抖和节流的区别:

  • 防抖在连续触发时只执行最后一次,而节流在连续触发时按照固定的时间间隔执行。
  • 防抖是等待一段时间后执行,节流是每隔一段时间执行。
  • 防抖适用于等待用户停止操作后执行的场景,节流适用于限制函数执行频率的场景。
  • 防抖保证只有最后一次触发才会执行函数,节流在指定的时间间隔内执行一次,并忽略其他触发。

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

  深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是在复制对象或数组时常用的两种方式,它们的区别在于复制后是否创建新的引用。

区别:

  • 浅拷贝创建一个新的对象或数组,并将原始对象或数组的引用复制给新对象或数组。这意味着新旧对象或数组仍然共享相同的内部数据,修改其中一个对象或数组会影响到另一个。
  • 深拷贝创建一个全新的对象或数组,并将原始对象或数组的所有嵌套对象和数组进行递归复制。这意味着新对象或数组与原始对象或数组完全独立,修改一个不会影响到另一个。
    下面是一些实现深拷贝和浅拷贝的方法:

实现浅拷贝的方法:

  • 扩展运算符(…):适用于浅拷贝对象或数组。
  • Object.assign():适用于浅拷贝对象。
  • Array.prototype.slice():适用于浅拷贝数组。
  • Array.prototype.concat():适用于浅拷贝数组。
// 浅拷贝示例
// 使用扩展运算符(...)
const originalObj = { name: 'John', age: 25 };
const shallowCopyObj = { ...originalObj };

// 使用Object.assign()
const originalObj = { name: 'John', age: 25 };
const shallowCopyObj = Object.assign({}, originalObj);

// 使用Array.prototype.slice()
const originalArray = [1, 2, 3, 4, 5];
const shallowCopyArray = originalArray.slice();

// 使用Array.prototype.concat()
const originalArray = [1, 2, 3, 4, 5];
const shallowCopyArray = originalArray.concat();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

实现深拷贝的方法:

  • 递归方法:递归遍历对象或数组的每个属性,进行复制并递归处理嵌套对象或数组。
  • JSON.parse(JSON.stringify()):将对象或数组转换为JSON字符串,然后再将其转换回对象或数组,实现深拷贝。但该方法有一些限制,例如无法拷贝函数和循环引用。
// 深拷贝示例
// 递归方法
function deepCopy(obj) {
 if (typeof obj !== 'object' || obj === null) {
   return obj;
 }

 let copy;
 if (Array.isArray(obj)) {
   copy = [];
   for (let i = 0; i < obj.length; i++) {
     copy[i] = deepCopy(obj[i]);
   }
 } else {
   copy = {};
   for (let key in obj) {
     if (obj.hasOwnProperty(key)) {
       copy[key] = deepCopy(obj[key]);
     }
   }
 }

 return copy;
}

const originalObj = { name: 'John', age: 25, hobbies: ['reading', 'cooking'] };
const deepCopyObj = deepCopy(originalObj);

// JSON.parse(JSON.stringify())
const originalObj = { name: 'John', age: 25, hobbies: ['reading', 'cooking'] };
const deepCopyObj = JSON.parse(JSON.stringify(originalObj));

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

需要注意的是,使用递归方法实现深拷贝时要注意处理循环引用的情况,以避免陷入无限循环。而使用JSON.parse(JSON.stringify())方法实现深拷贝时,无法拷贝函数和特殊的对象属性(如原型链上的属性)。

10. 对比 一下var、const、let。

  var、let 和 const 是 JavaScript 中用于声明变量的关键字,它们有一些区别:

  • 变量提升:使用 var 声明的变量存在变量提升,即在声明语句之前就可以访问变量。而 letconst 声明的变量不存在变量提升,只能在声明语句之后才能访问。

  • 作用域:var 声明的变量具有函数作用域,即变量在整个函数内部都可见。而letconst 声明的变量具有块级作用域,即变量在声明所在的块(如 {})内有效,超出块的范围就无法访问。

  • 重复声明:使用 var 声明的变量可以被重复声明,而且不会报错。而 letconst 声明的变量不允许重复声明,如果重复声明同一个变量会报错。

  • 可变性:var let声明的变量可以被修改和重新赋值,值是可变的。而 const 声明的变量是常量,一旦赋值后就不能再修改

  • 声明时初始化:var 声明的变量在声明时不需要立即初始化,可以在后续代码中进行赋值。而 let const 声明的变量在声明时没有赋初值会被自动初始化为 undefined
      综上所述,var 是 ES5 中使用的变量声明方式,let 和 const 是 ES6 引入的块级作用域变量声明方式。推荐使用 let 和 const 来声明变量,因为它们更安全、更符合现代 JavaScript 的最佳实践。只有在特定情况下才需要使用 var,例如需要兼容旧版浏览器或在特定的函数作用域中需要变量提升的情况。

11. ES next新特性有哪些?

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

  • 语法简洁:箭头函数具有更简洁的语法形式。它们可以使用箭头符号 => 来定义函数,省略了 function 关键字和函数体内的大括号。当函数体只有一条返回语句时,可以省略 return 关键字。

  • 没有自己的 this 绑定:箭头函数没有自己的 this 绑定,它们会继承外层作用域的 this 值。这意味着在箭头函数内部,无法通过 this 访问到自身的执行上下文,而是使用外层作用域的 this。

  • 没有原型:箭头函数没有自己的 prototype 属性,因此不能用作构造函数来创建对象。普通函数可以作为构造函数使用,并且具有自己的原型对象。

  • 不能用作方法:由于箭头函数没有自己的 this 绑定,所以不能用作对象的方法。普通函数在对象上作为方法调用时,this 会指向该对象。

  • 没有 arguments 对象:箭头函数没有自己的 arguments 对象,它们会继承外层作用域的 arguments 对象。如果需要使用参数集合,可以使用剩余参数语法 …args 来获取函数的参数列表。

  总体而言,箭头函数适用于简单的函数表达式和回调函数的定义,以及需要继承外层作用域的 this 值的情况。普通函数则具有更多的灵活性,可以用作构造函数、对象的方法,并且具有自己的 this 和 prototype。选择使用哪种函数取决于具体的使用场景和需求。

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

使用 new 关键字创建对象的过程如下:

  • 创建一个空对象:通过 new 关键字创建一个新的空对象,这个对象是一个空的 JavaScript 对象(Object)。

  • 设置对象的原型链:将新创建的对象的原型链指向构造函数的原型对象。可以通过访问构造函数的 prototype 属性来获取它的原型对象。

  • 执行构造函数:将新创建的对象作为this上下文,并执行构造函数。这样构造函数中的代码就可以操作并修改新对象的属性和方法。

  • 返回新对象:如果构造函数中没有显式返回其他对象,那么new表达式将隐式地返回新创建的对象。

下面是一个示例,展示使用 new 关键字创建对象的过程:

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

// 使用 new 关键字创建对象
var person1 = new Person('Alice', 25);

console.log(person1); // 输出:Person { name: 'Alice', age: 25 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  在上面的示例中,使用 new 关键字创建了一个名为 Person 的构造函数。然后,通过 new Person('Alice', 25) 创建了一个新的 Person 对象,并将其赋值给变量 person1person1 对象拥有name age属性,这些属性是通过构造函数中的代码赋值给对象的。最后,person1 对象被打印出来,显示其属性值。
  使用 new 关键字创建对象时,会执行构造函数,并将构造函数中的代码作用于新创建的对象上。这使得我们可以在构造函数中初始化对象的属性,并在需要时添加其他逻辑。

14. this指向系列问题。

  关于 JavaScript 中的 this 关键字,常常会引发一些困惑。下面是一系列与 this 相关的问题和解答:

  1. this 的值是如何确定的?
  • this 的值是在函数被调用时确定的,它指向调用函数的上下文对象。具体的绑定规则取决于函数的调用方式。
  1. this 的绑定规则有哪些?
  • 默认绑定:在独立函数调用时,this 绑定到全局对象(浏览器中是 window 对象,Node.js 中是 global 对象)。
  • 隐式绑定:当函数作为对象的方法调用时,this 绑定到调用该方法的对象。
  • 显式绑定:使用 callapplybind方法可以显式地指定函数的this值。
  • new 绑定:使用 new 关键字创建对象时,this 绑定到新创建的对象。
  • 箭头函数绑定:箭头函数的 this 值继承自外层作用域,没有自己的this绑定。
  1. 如何解决this指向问题?
  • 使用箭头函数:箭头函数的this绑定是词法作用域,继承自外层作用域。
  • 使用 bind 方法:通过bind方法将函数绑定到指定的上下文对象。
  • 使用 call apply 方法:通过 call 或 apply 方法显式地指定函数的this值。
  • 使用类的实例方法:使用类的实例方法时,this 默认绑定到实例对象。
  • 使用闭包:通过创建闭包,在闭包内部访问所需的上下文对象。
  1. 箭头函数与普通函数的 this 有何区别?
  • 箭头函数没有自己的 this 绑定,它继承自外层作用域的 this 值。
  • 普通函数的 this 值根据调用方式不同而变化,可以通过 bindcall apply 显式地绑定。

  理解 this 的绑定规则和如何解决this指向问题对于编写正确的 JavaScript 代码非常重要。需要注意不同的调用方式和上下文,以确定 this 的准确值。

15. 手写bind方法。(未完)

  bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
  下面是一个手写的简化版 bind 方法的实现:

Function.prototype.myBind = function (context, ...args) {
  const fn = this; // 原函数
  return function (...innerArgs) {
    return fn.apply(context, args.concat(innerArgs));
  };
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这个简化版的 myBind 方法实现了以下功能:

myBind方法添加到 Function.prototype,使得所有函数都可以调用该方法。

接受一个context参数,表示需要绑定的上下文对象。

返回一个新的函数,该函数会在调用时将 context 绑定为其执行上下文。

新函数内部使用 apply 方法调用原函数 fn,并传递绑定的 context 和合并后的参数列表。

16. 谈谈对闭包的理解?什么是闭包?闭包有哪些应用场景

  闭包是指在函数内部创建的函数,并且这个内部函数可以访问到其外部函数的变量、参数和作用域链。换句话说,闭包是一个函数以及它被创建时所处的词法环境的组合体。

  理解闭包的关键是要理解 JavaScript 中的作用域和词法环境。每当函数被创建时,它会创建一个自己的作用域,并且在函数内部定义的变量和函数都可以在该作用域内访问。当函数返回时,其内部作用域并不会被销毁,而是被保存下来,并与函数形成闭包。

闭包的主要特点包括:

  1. 内部函数可以访问外部函数的变量和参数。
  2. 闭包可以持有对外部函数作用域的引用,使得外部函数的变量和作用域链不会被销毁。
  3. 闭包可以延长变量的生命周期,使得在外部函数执行完毕后仍然可以访问和操作其内部变量。

闭包有许多应用场景,包括但不限于:

  • 封装私有变量:通过闭包可以创建私有变量,使其无法被外部访问,只能通过内部函数进行操作。
  • 记忆化:利用闭包可以缓存函数的计算结果,提高函数的执行效率。
  • 模块化开发:使用闭包可以创建模块化的代码结构,将一些变量和函数私有化,只暴露需要的接口给外部。
  • 实现回调和事件处理:通过闭包可以在异步操作中保存上下文和状态信息,实现回调函数或事件处理函数。
  • 创建迭代器和生成器:利用闭包可以实现迭代器和生成器,用于遍历数据集合或生成序列。
  • 防止变量污染:使用闭包可以创建独立的作用域,避免变量冲突和全局命名空间污染。

  需要注意的是,闭包可能会导致内存泄漏问题,因为闭包会持有对外部函数作用域的引用,导致一些变量无法被及时释放。因此,在使用闭包时需要注意及时释放不再需要的引用,避免造成不必要的内存消耗。

17. 闭包有什么缺点?如何避免闭包?

  闭包虽然有许多优点和应用场景,但也存在一些缺点,主要包括以下几个方面:

  • 内存消耗:闭包会持有对外部函数作用域的引用,导致外部函数的变量无法被及时释放,从而增加了内存的消耗。如果闭包被频繁创建且没有被妥善管理,可能会导致内存泄漏问题。

  • 性能影响:由于闭包涉及对外部作用域的引用,每当闭包被调用时都需要查找和访问外部作用域中的变量。这个过程相比于直接访问本地变量会增加一定的性能开销。

  • 难以理解和维护:闭包会使代码的作用域变得复杂,增加了代码的难度,特别是在涉及多层嵌套的闭包时,可能会导致代码可读性和可维护性的降低。

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

  • 适度使用闭包:只在必要的情况下使用闭包,避免滥用。合理评估闭包的优劣势,并权衡是否值得引入闭包。

  • 及时释放闭包:当不再需要使用闭包时,应手动解除对外部作用域的引用,确保相关变量可以被垃圾回收机制回收,避免内存泄漏。

  • 使用模块化开发:尽量使用模块化的代码结构,将变量和函数的作用域限制在需要的范围内,减少闭包的使用。

  • 减少不必要的变量引用:在闭包中尽量避免引用大量的外部变量,只引用必要的变量,以减少内存消耗和性能开销。

  • 使用现代 JavaScript 特性:使用 let 和 const 关键字声明变量,利用块级作用域来代替闭包的使用,以避免一些潜在的问题。
      总之,闭包是 JavaScript 强大的特性之一,但在使用时需要注意潜在的问题,并遵循最佳实践,以确保代码的性能和可维护性。

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

  JavaScript 事件循环(Event Loop)是 JavaScript 引擎用于处理异步操作的机制。它负责管理和调度 JavaScript 代码中的任务执行顺序,确保代码按照正确的顺序执行,并且处理异步操作的结果。

  事件循环的基本原理是将任务分为两类:同步任务和异步任务。同步任务会在代码中按照顺序执行,而异步任务会被放置在任务队列中,等待执行。任务队列分为多个队列,包括宏任务队列(Macrotask Queue)和微任务队列(Microtask Queue)。

事件循环的过程如下:

  1. 执行同步任务:JavaScript 引擎按照代码的顺序执行同步任务,直到遇到第一个异步任务。
  2. 将异步任务放入任务队列:当遇到异步任务时,将其添加到相应的任务队列中,比如宏任务队列或微任务队列。
  3. 执行微任务:当同步任务执行完毕或遇到特定的异步任务时,JavaScript 引擎会检查微任务队列是否为空。如果微任务队列不为空,则依次执行队列中的任务,直到微任务队列被清空(这意味着微任务的执行具有优先级,会在下一个宏任务之前完成)。常见的微任务包括 Promise 的回调函数MutationObserver 的回调函数等。
  4. 执行宏任务:当微任务执行完毕后,JavaScript 引擎会选择宏任务队列中的第一个任务,并执行它。常见的宏任务包括 setTimeout、setInterval、DOM 事件等。
  5. 重复以上步骤:重复执行上述步骤,不断从任务队列中取出任务并执行,直到所有任务执行完毕。
      事件循环的关键是要理解同步任务和异步任务的执行顺序以及任务队列的作用。通过合理使用异步任务和任务队列,可以实现非阻塞的异步编程,提高 JavaScript 的执行效率和用户体验。
      需要注意的是,事件循环机制的细节可能会因不同的 JavaScript 运行环境(比如浏览器和 Node.js)而有所不同,但基本原理是相似的。理解事件循环机制对于编写高效、可靠的异步代码至关重要。

19. 谈谈对promise理解?

  Promise 是 JavaScript 中用于处理异步操作的对象。它是 ECMAScript 6 (ES6) 引入的一种异步编程解决方案,旨在简化异步操作的处理流程,使代码更加清晰、可读性更高。

Promise 有以下特点和优势:

  1. 异步操作的封装:Promise 将异步操作封装成一个对象,使得异步操作更加直观和易于管理。它提供了一种统一的接口,让开发者可以更加方便地处理异步任务。

  2. 链式调用:Promise 支持链式调用,可以在一个 Promise 完成后继续执行下一个 Promise。这种链式调用的方式使得异步代码可以像同步代码一样表达,提高了代码的可读性和可维护性。

  3. 异常处理:Promise 提供了 catch 方法用于捕获和处理异常,避免了回调地狱(回调地狱指的是多层嵌套的回调函数,代码结构变得混乱且难以维护)中异常处理的问题。在链式调用中,任何一个 Promise 发生错误时都可以通过 catch 方法捕获并进行相应的处理。

  4. 解决回调地狱问题:Promise 的链式调用可以有效解决回调地狱(Callback Hell)的问题,使得代码更加清晰、结构更加简洁。通过链式调用,可以按照顺序组织异步操作,避免多层嵌套的回调函数。

  假设有以下场景:首先进行异步操作A,然后根据操作A的结果执行异步操作B,最后根据操作B的结果执行异步操作C。在回调函数中处理这种情况时,代码会嵌套多层,例如:

doAsyncOperationA(function (resultA, errorA) {
  if (errorA) {
    // 处理A操作的错误
  } else {
    doAsyncOperationB(resultA, function (resultB, errorB) {
      if (errorB) {
        // 处理B操作的错误
      } else {
        doAsyncOperationC(resultB, function (resultC, errorC) {
          if (errorC) {
            // 处理C操作的错误
          } else {
            // 所有操作完成
          }
        });
      }
    });
  }
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

  这样的代码结构不仅难以阅读和理解,而且容易出错,尤其是在处理异常时。每个异步操作的错误都需要在嵌套的回调函数中进行处理,增加了代码的复杂性和维护成本。

  而使用 Promise 可以避免回调地狱中异常处理的问题。通过 Promise 的链式调用和异常处理机制,可以将上述代码重写为如下形式:

doAsyncOperationA()
  .then(function (resultA) {
    return doAsyncOperationB(resultA);
  })
  .then(function (resultB) {
    return doAsyncOperationC(resultB);
  })
  .catch(function (error) {
    // 处理任何一个操作的错误
  });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

  使用 Promise,每个异步操作都返回一个 Promise 对象,可以通过then方法连接多个操作,并通过 catch 方法统一捕获和处理异常。这样代码结构更加清晰,异常处理也更加简洁和集中,避免了回调地狱中异常处理的问题。

  通过使用 Promise,我们可以更好地组织和管理异步任务,使代码更加可读、可维护,并提供了更好的异常处理机制,提高了代码的健壮性和可靠性。

19.1 使用 Promise 的基本流程如下:

  1. 创建 Promise 对象:通过 new 关键字和 Promise 构造函数创建一个 Promise 对象,并将异步操作封装在 Promise 的执行器函数中。

  2. 执行异步操作:在Promise的执行器函数中执行异步操作,可以是一个 AJAX 请求、定时器操作或其他任何异步任务。

  3. 返回结果或抛出异常:异步操作完成后,通过调用 resolve 方法返回操作结果,或者通过调用 reject 方法抛出异常。

  4. 处理结果:通过调用 then 方法来处理 Promise 的执行结果,可以使用链式调用方式处理多个异步任务。

  5. 错误处理:通过调用 catch 方法捕获可能发生的异常,并进行相应的处理。

  6. Promise 的使用可以大大简化异步操作的处理流程,使得代码更加清晰、可读性更高。它已经成为现代 JavaScript 开发中处理异步任务的常用工具,广泛应用于各种场景,包括网络请求、文件读写、定时器操作等。

19.2 Promise 对象的三种状态:

  • Pending(进行中):Promise 对象初始的状态,表示异步操作正在进行中,还没有成功也没有失败。
  • Fulfilled(已成功):表示异步操作已经成功完成,并且返回了一个结果值。一旦 Promise 进入 Fulfilled 状态,它就会保持这个状态,不会再改变。
  • Rejected(已失败):表示异步操作发生了错误或失败。一旦 Promise 进入 Rejected 状态,它也会保持这个状态,不会再改变。

  Promise 对象的状态只能从 Pending 转变为 Fulfilled 或 Rejected,并且一旦转变为其中一种状态,就无法再改变。当 Promise 的状态发生变化时,会触发相应的回调函数。
  在 Pending 状态下,可以通过调用 Promise 实例的 resolve 方法将 Promise 状态转变为 Fulfilled,并传递一个结果值作为参数。同样地,调用 reject 方法可以将 Promise 状态转变为 Rejected,并传递一个错误对象作为参数。
  一旦 Promise 的状态转变为 Fulfilled 或 Rejected,会立即执行相应状态的回调函数。通过调用 then 方法注册的回调函数会被放入一个队列中,等待 Promise 状态变为对应的状态时执行。如果在注册回调之前,Promise 的状态已经变为 Fulfilled 或 Rejected,则回调函数会立即执行。
  需要注意的是,Promise 的状态一旦改变就是不可逆的,而且状态的改变是异步的。这意味着在创建 Promise 对象后,可以通过 then 方法注册回调函数,而这些回调函数会在 Promise 状态改变之后执行。这样可以更好地处理异步操作,使代码更具可读性和可维护性。

20. 手写 Promise。

class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.callbacks = [];

    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.callbacks.forEach((callback) => this.executeCallback(callback));
      }
    };

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.value = reason;
        this.callbacks.forEach((callback) => this.executeCallback(callback));
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  executeCallback(callback) {
    const { onFulfilled, onRejected, resolve, reject } = callback;
    try {
      if (this.state === 'fulfilled') {
        typeof onFulfilled === 'function' ? resolve(onFulfilled(this.value)) : resolve(this.value);
      } else if (this.state === 'rejected') {
        typeof onRejected === 'function' ? resolve(onRejected(this.value)) : reject(this.value);
      }
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      const callback = { onFulfilled, onRejected, resolve, reject };
      if (this.state === 'pending') {
        this.callbacks.push(callback);
      } else {
        this.executeCallback(callback);
      }
    });
  }

  catch(onRejected) {
    return this.then(undefined, onRejected);
  }

  static resolve(value) {
    return new MyPromise((resolve) => resolve(value));
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      const results = [];
      let completedCount = 0;

      const processResult = (index, value) => {
        results[index] = value;
        completedCount++;

        if (completedCount === promises.length) {
          resolve(results);
        }
      };

      for (let i = 0; i < promises.length; i++) {
        promises[i]
          .then((value) => processResult(i, value))
          .catch((reason) => reject(reason));
      }
    });
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      for (let i = 0; i < promises.length; i++) {
        promises[i]
          .then((value) => resolve(value))
          .catch((reason) => reject(reason));
      }
    });
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97

21. 实现 Promise.all方法。(未完)

  Promise.all 接收一个promise对象的数组作为参数,当这个数组里的所有promise对象全部变为resolve或 有 reject 状态出现的时候,它才会去调用 .then 方法,它们是并发执行的。

function myPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let fulfilledCount = 0;

    for (let i = 0; i < promises.length; i++) {
      promises[i]
        .then(result => {
          results[i] = result;
          fulfilledCount++;

          if (fulfilledCount === promises.length) {
            resolve(results);
          }
        })
        .catch(reject);
    }
  });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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

  在 TypeScript 中,type 和 interface 都用于定义类型,但它们在一些方面有一些区别。

  1. 语法形式不同:
  • type 使用 type 关键字进行定义,例如:type MyType = { prop: string };
  • interface 使用 interface 关键字进行定义,例如:interface MyInterface { prop: string; }
  1. 类型扩展不同:
  • type 可以使用联合类型(|)和交叉类型(&)来组合类型,例如:type MyType = TypeA | TypeB;
  • interface 不支持直接的联合类型和交叉类型,但可以使用继承来扩展其他接口,例如:interface MyInterface extends InterfaceA, InterfaceB { }
  1. 兼容性检查不同:
  • type 对于兼容性检查是基于结构化类型,即只要属性和类型匹配即可认为是兼容的。
  • interface 对于兼容性检查是基于鸭子类型()即只要接口的必需属性和类型能够被目标对象满足即可认为是兼容的。
  1. 可读性和可维护性:
  • interface 更适合用于描述对象的形状和结构,以及类的实现。
  • type 更适合用于创建复杂的类型别名,或者使用联合类型、交叉类型、映射类型等高级类型。

  总体而言,interface 更常用于对象的定义和类的实现,而 type 更适用于创建复杂的类型别名和组合类型。在实际使用中,可以根据具体的需求和语境选择使用哪种形式来定义类型。

23. 讲讲Typescript中的泛型?

  在 TypeScript 中,泛型(Generics)是一种参数化类型的机制,它允许我们在定义函数、类、接口等可重用组件时使用变量来表示类型,从而增加代码的灵活性和可重用性。

  通过使用泛型,我们可以编写与类型无关的代码,这样我们可以在不同的地方使用相同的逻辑,但适用于不同的类型。泛型可以用于函数参数、函数返回值、类成员、接口等多个场景。

以下是一些泛型的使用示例:

  1. 函数泛型:
function identity<T>(arg: T): T {
  return arg;
}

// 使用泛型函数
let result = identity<string>('Hello');
console.log(result); // 输出:Hello

// TypeScript 会根据参数类型进行类型推断
let result2 = identity(42);
console.log(result2); // 输出:42
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

2.类泛型:

class ArrayWrapper<T> {
  private array: T[];

  constructor() {
    this.array = [];
  }

  push(item: T) {
    this.array.push(item);
  }

  getItems(): T[] {
    return this.array;
  }
}

// 使用泛型类
const wrapper = new ArrayWrapper<number>();
wrapper.push(1);
wrapper.push(2);
wrapper.push(3);
const items = wrapper.getItems();
console.log(items); // 输出:[1, 2, 3]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  1. 接口泛型:
interface Pair<T, U> {
  first: T;
  second: U;
}

// 使用泛型接口
const pair: Pair<string, number> = {
  first: 'Hello',
  second: 42
};
console.log(pair); // 输出:{ first: 'Hello', second: 42 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

  通过使用泛型,我们可以编写更通用的代码,同时保持类型安全。泛型可以适用于各种场景,例如容器类、算法函数、异步处理等,让我们能够处理不同类型的数据并保持类型的一致性。

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

  在 TypeScript 中,可以使用函数重载(Function Overloading)来定义多个函数签名(函数签名是指函数的声明或定义的形式,包括函数的名称、参数列表和返回类型。它描述了函数的输入和输出。),以允许函数接受不同数量或类型的参数并返回不同类型的值。函数重载可以提供更严格的类型检查,并增加代码的可读性。

下面是一个示例,展示如何在 TypeScript 中实现函数重载:

function foo(x: number): number;
function foo(x: string): string;
function foo(x: number | string): number | string {
  if (typeof x === 'number') {
    return x * 2;
  } else if (typeof x === 'string') {
    return x.toUpperCase();
  }
}

// 示例使用
const result1 = foo(10); // 返回类型为 number
const result2 = foo('hello'); // 返回类型为 string
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

  在上述示例中,函数foo使用了函数重载。首先,我们定义了两个函数签名,分别接受一个 number 类型的参数并返回 number 类型,以及一个 string 类型的参数并返回 string 类型。然后,我们在函数实现中使用联合类型number | string来处理传入参数的类型,并根据不同的情况返回不同类型的值。

  通过函数重载,TypeScript 编译器能够根据调用时的参数类型进行正确的类型推断和类型检查,从而提供更强大的静态类型检查和类型安全性。

  需要注意的是,函数重载要求函数实现是上述函数签名的一种实现,否则会导致编译错误。在实现中,我们可以根据需要进行逻辑处理,但需要确保函数实现的返回类型与函数签名中指定的返回类型相符。

25. CmmonJS和ESM区别?

  CommonJS(简称CJS)和ESM(ES Modules,即 ECMAScript 模块)是两种不同的模块化规范,用于在 JavaScript 中组织和导出/导入模块。

以下是它们之间的主要区别:

  • 语法差异:CJS 使用 require() module.exports语法来导入和导出模块,而 ESM 使用importexport 语法来实现相同的功能。ESM 的语法更加直观和简洁,更接近其他编程语言的模块化语法。

  • 动态与静态:CJS 模块是动态加载的,意味着模块的加载发生在运行时。而 ESM 模块是静态加载的,模块的加载在代码解析阶段就确定了。这使得 ESM 模块在编译时可以进行更好的优化和静态分析。

  • 异步加载:ESM 具有原生的异步加载能力,可以使用import()动态加载模块,而 CJS 不支持原生的异步加载方式。

  • 变量绑定:在 ESM 中,导入的模块是只读的,不能直接修改导入的变量值。而 CJS 中的导入变量是可修改的。

  • 顶层作用域:在 CJS 中,模块代码运行在一个单独的函数作用域中,每个模块都有自己的作用域。而在 ESM 中,模块代码运行在顶层作用域中,可以直接访问全局变量和其他模块的顶层变量。

  需要注意的是,CJS 主要用于服务器端的 Node.js 环境,而 ESM 是 ECMAScript 标准的一部分,逐渐被现代的 JavaScript 运行环境和浏览器所支持。在浏览器环境中,可以使用 ESM 模块进行前端开发。

  为了兼容性和平滑过渡,Node.js 也开始支持 ESM 模块,并提供了 importexport 语法的实验性支持。可以通过文件扩展名为 .mjs 或配置package.json中的 “type”: “module” 来使用 ESM 模块。

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

  柯里化(Currying)是一种函数转换的技术,它将接受多个参数的函数转换为一系列只接受单个参数的函数。通过柯里化,我们可以将原本需要多个参数的函数调用转化为一系列只需传递一个参数的函数调用。

柯里化的作用包括:

  • 参数复用:柯里化可以通过部分应用(Partial Application)的方式,先固定一部分参数,返回一个接受剩余参数的新函数。这样可以复用函数的部分参数,提高代码的重用性和灵活性。
  • 延迟执行:柯里化可以将一个需要多个参数的函数,转化为多个只接受单个参数的函数。这样可以实现参数的延迟传递,使得函数的执行可以根据实际需要进行延迟。

  实现柯里化的一种常见方式是使用闭包和递归。以下是一个简单的柯里化函数的实现示例:

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function (...moreArgs) {
        return curried.apply(this, args.concat(moreArgs));
      };
    }
  };
}

// 示例使用
function add(a, b, c) {
  return a + b + c;
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 输出: 6
console.log(curriedAdd(1, 2)(3)); // 输出: 6
console.log(curriedAdd(1)(2, 3)); // 输出: 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

  在上述代码中,curry函数接受一个函数fn作为参数,并返回一个柯里化后的函数。在返回的柯里化函数 curried 中,首先判断传入参数的个数是否达到原始函数fn的参数个数,如果是则直接执行函数 fn,否则返回一个新的函数,将传入的参数和后续传入的参数进行拼接,再递归调用 curried 函数。

  通过这种方式,我们可以通过多次调用返回的柯里化函数,逐渐传递参数,并在最后一次调用中执行原始函数。这样可以实现函数的柯里化,并实现参数的复用和延迟执行的效果。

  需要注意的是,上述的柯里化函数实现是一个简单示例,仅适用于固定参数个数的情况。对于参数个数可变的函数或需要处理多个参数的情况,需要进行相应的扩展和调整。

27. 讲讲js垃圾回收机制。

  JavaScript 中的垃圾回收(Garbage Collection)是一种自动内存管理机制,用于释放不再被程序使用的内存空间,以防止内存泄漏和提高内存利用效率。

JavaScript 的垃圾回收机制基于以下两个主要原理:

  • 引用计数:最早的垃圾回收机制之一是引用计数(Reference Counting)。每个对象都有一个引用计数器,当有一个引用指向该对象时,引用计数就加一;当引用失效或被重置时,引用计数就减一。当引用计数为零时,表示该对象不再被引用,即可被回收。然而,引用计数机制无法处理循环引用的情况,即使两个对象彼此引用,但无法被外部访问到,它们的引用计数仍然不为零,导致内存泄漏。

  • 标记-清除:标记-清除(Mark and Sweep)是目前主流的垃圾回收算法。它的基本思想是通过标记那些仍然在使用的对象,然后清除未标记的对象。垃圾回收器会从根对象开始遍历,标记所有可以访问到的对象,而无法访问到的对象就会被认为是垃圾,进行清除操作。这种算法可以处理循环引用的情况,并且只保留活动的对象,释放无用的对象。

JavaScript 的垃圾回收器通常使用标记-清除算法结合其他优化技术来进行垃圾回收。除了标记-清除算法,还有一些其他的垃圾回收算法,如分代回收(Generational Collection)、增量标记(Incremental Marking)等。

  垃圾回收器在后台运行,周期性地检查内存中的对象,标记和清除不再被引用的对象,从而释放内存空间。具体的触发时机和策略由具体的 JavaScript 引擎实现决定,一般会根据内存使用情况和执行情况来动态调整。

  需要注意的是,虽然垃圾回收机制可以自动管理内存,但不合理的内存使用和引用管理仍可能导致内存泄漏或性能问题。因此,编写高效的 JavaScript 代码时,仍然需要注意避免不必要的引用和循环引用,合理释放不再使用的资源,以优化内存的使用和程序的性能。

28. 实现一个发布订阅。

  发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。

  • 订阅者(Subscriber)把自己想订阅的事件 注册(Subscribe)到调度中心(Event Channel);
  • 当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由 调度中心 统一调度(Fire Event)订阅者注册到调度中心的处理代码。

◾ 例子
  比如我们很喜欢看某个公众号的文章,但是不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。
  上面一个看似简单的操作,其实是一个典型的发布订阅模式,公众号属于发布者,用户属于订阅者;用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。

◾ 发布/订阅模式的优点是对象之间解耦,异步编程中,可以更松耦合的代码编写;
◾ 缺点是创建订阅者本身要消耗一定的时间和内存,虽然可以弱化对象之间的联系,多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护。

class EventEmitter {
  constructor() {
    this.events = {};
  }

  // 订阅事件
  subscribe(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  // 发布事件
  publish(eventName, ...args) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(callback => {
        callback(...args);
      });
    }
  }

  // 取消订阅
  unsubscribe(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
const eventEmitter = new EventEmitter();

// 订阅事件
eventEmitter.subscribe('event1', data => {
  console.log('Event 1:', data);
});

eventEmitter.subscribe('event2', () => {
  console.log('Event 2');
});

// 发布事件
eventEmitter.publish('event1', 'Hello World');
// Output: Event 1: Hello World

eventEmitter.publish('event2');
// Output: Event 2

// 取消订阅
const callback = () => {
  console.log('Callback');
};

eventEmitter.subscribe('event3', callback);

eventEmitter.unsubscribe('event3', callback);

eventEmitter.publish('event3');
// No output (Callback not called)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

29. 如何实现数组怕平(扁平化)?(未完)

  概念很简单,意思是将一个“多维”数组降维,比如:

// 原数组是一个“三维”数组
const array = [1, 2, [3, 4, [5, 6], 7], 8, 9]

// 可以降成二维
newArray1 = [1, 2, 3, 4, [5, 6], 7, 8, 9]

// 也可以降成一维
newArray2 = [1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

  数组拍平也称数组扁平化、数组降维。

  1. 使用递归:
      使用递归方法可以处理任意层级的嵌套数组。以下是使用递归实现数组扁平化的示例代码:
function flattenArray(arr) {
  let result = [];
  arr.forEach(item => {
    if (Array.isArray(item)) {
      result = result.concat(flattenArray(item));
    } else {
      result.push(item);
    }
  });
  return result;
}

const nestedArray = [1, [2, [3, 4], 5], 6];
const flattenedArray = flattenArray(nestedArray);
console.log(flattenedArray); // 输出: [1, 2, 3, 4, 5, 6]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

  在上述代码中,flattenArray 函数使用递归的方式对数组进行遍历,对于每个元素,如果是数组则递归调用 flattenArray 函数进行扁平化,否则将其添加到结果数组中。最后返回结果数组。

2.reduce是JS数组中非常强大的一个方法,同时也是JS中的一个函数式编程API。

上面的递归实现的关键就是对数组的每一项进行处理,遇到数组就递归处理它,既然需要循环和结果数组,那么我们可以使用reduce来简化我们的代码:

let arr = [1, [2, [3, 4]]];
function flatten(arr) {
    return arr.reduce(function(pre, cur){
        return pre.concat(Array.isArray(cur) ? flatten(cur) : cur)
    }, [])
}
console.log(flatten(arr));//  [1, 2, 3, 4,5]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

  使用reduce后,代码更加的简洁,reduce 的第一个参数用来返回最后累加的结果,第二个参数是当前遍历到的元素值,处理数组元素和非数组元素的思路和第一种方法是一样,最后再把处理后的结果拼接到累加的结果数组中返回即可。

3.扩展运算符是ES6的新特性之一,用它操作数组可以直接展开数组的第一层,利用这个特性,我们可以不使用递归来实现数组的展平,这是因为每一次递归都是对当前层次数组的一次展开,而扩展操作符就是干这工作的。

let arr = [1, [2, [3, 4]]];
function flatten(arr) {
    while (arr.some(i => Array.isArray(i))) {
        arr = [].concat(...arr);
    }
    return arr;
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

代码中使用了数组的另一个方法some,目的是判断当前数组是否还有数组元素,如果有则对数组进行一层展开,同时将展开结果作为下一次判断的条件,这样就像剥洋葱一样,一层层地剥开洋葱皮,当循环条件不满足时说明数组里已经没有数组元素了,这是数组已经完全扁平。

30. 如何实现数组去重?

  1. 使用 Set 数据结构:
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = Array.from(new Set(array));
console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]
  • 1
  • 2
  • 3

  使用 Set 数据结构可以快速去除数组中的重复项。通过将数组转换为 Set,然后将 Set 转换回数组,就可以得到去重后的数组。
注:Set 是 JavaScript 中的一种数据结构,它用于存储唯一的值,即不允许重复的元素。Set 对象可以用来存储任意类型的值,包括原始类型和引用类型。

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

  利用 filter 方法遍历数组,通过indexOf方法判断当前元素在数组中的第一个位置与当前索引是否一致,如果一致则保留该元素,从而实现去重。

  1. 使用 reduce 方法:
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = array.reduce((acc, curr) => {
  if (!acc.includes(curr)) {
    acc.push(curr);
  }
  return acc;
}, []);
console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

  利用 reduce 方法遍历数组,通过判断累加器中是否包含当前元素,如果不包含则将其添加到累加器中,从而实现去重。

  1. 使用 Map 数据结构:
const array = [1, 2, 2, 3, 4, 4, 5];
const uniqueArray = Array.from(new Map(array.map(value => [value, value])).values());
console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]

  • 1
  • 2
  • 3
  • 4

  通过使用 Map 数据结构,将数组元素作为键和值存储在 Map 中,然后取出 Map 的值部分生成去重后的数组。
注:Map 是 JavaScript 中的一种数据结构,它用于存储键值对的集合,其中键是唯一的,而值可以重复。Map 可以使用任意类型的值作为键,并且可以迭代获取每个键值对。

  这些方法都可以实现数组去重,选择哪种方法取决于具体的需求和性能要求。需要注意的是,这些方法在处理对象或复杂数据类型的数组时可能会出现问题,因为它们对于对象的比较是基于引用的。在处理对象数组时,需要考虑自定义比较函数或者使用深拷贝的方式来进行去重。

31. 如何判定对象是null类型

  要判定一个对象是否为 null 类型,可以使用严格相等运算符===将对象与null进行比较。以下是判定对象是否为 null 的示例代码:

const obj = null;
if (obj === null) {
  console.log("对象是 null 类型");
} else {
  console.log("对象不是 null 类型");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

32. =====的区别

  1. 数据类型比较:== 运算符在进行比较之前会进行类型转换,而 === 运算符在进行比较时会先检查数据类型。因此,== 运算符可能会在比较时进行隐式类型转换,而 === 运算符不会进行类型转换。

  2. 值比较:== 运算符在比较时会尝试将操作数转换为相同的类型,然后再进行值的比较。这可能导致一些奇怪的行为,例如将字符串和数字进行比较时会先将字符串转换为数字。而 === 运算符在进行比较时要求操作数的类型和值都相等。

  3. 严格性:=== 运算符是严格相等运算符,要求两个操作数的值和类型都完全相等才会返回 true。而 == 运算符是非严格相等运算符,在比较时会进行类型转换,如果操作数的值相等(或可转换为相等的值),则返回 true。

33. canva的一些图像操作为什么对图片有跨域限制

在浏览器中,涉及到对跨域图像进行操作的Canvas图像操作存在跨域限制,这是出于安全性的考虑。跨域资源共享(Cross-Origin Resource Sharing,简称CORS)是一种安全策略,用于限制跨域请求和访问资源。

浏览器实施同源策略,它要求网页的JavaScript只能访问与其来源相同的资源。在Canvas图像操作中,如果要对跨域图像进行像素级别的读取、修改或渲染等操作,会涉及到对图像数据的访问。这样的操作可能会导致信息泄露和安全风险,因此受到跨域限制。

具体来说,跨域图像数据操作存在以下限制:

读取像素数据:对于来自其他域的图像,使用getImageData()方法读取像素数据会受到跨域限制。

绘制图像:如果尝试使用drawImage()方法将来自其他域的图像绘制到Canvas上,也会受到跨域限制。

这些限制是浏览器为了保护用户隐私和确保安全而实施的安全策略。如果需要对跨域图像进行操作,通常需要满足以下条件之一:

跨域图像服务器设置了CORS头:如果图像资源的服务器配置了正确的CORS头,允许跨域请求,那么可以解除跨域限制。

使用代理:可以通过在自己的服务器上设置代理,将跨域图像请求发送到服务器上,然后将图像数据返回给客户端,以绕过跨域限制。

需要注意的是,这些限制只适用于JavaScript操作。对于通过标签展示的图像,不存在跨域限制,因为标签会自动处理跨域资源。但如果使用Canvas对标签中的图像进行操作,仍然会受到跨域限制。

总结而言,对于涉及到对跨域图像进行像素级别的读取、修改或渲染等操作,会受到浏览器的安全限制。要解除跨域限制,可以通过服务器设置CORS头或使用代理来处理。

34. null和undefined的区别

  总的来说 null undefined都代表空,主要区别在于 undefined 表示尚未初始化的变量的值,而 null 表示该变量有意缺少对象指向。

  • undefined
    这个变量从根本上就没有定义
    隐藏式 空值
  • null
    这个值虽然定义了,但它并未指向任何内存中的对象
    声明式 空值
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/238958
推荐阅读
相关标签
  

闽ICP备14008679号