【前端面试题】:JavaScript篇

1.JavaScript 是静态类型语言还是动态类型语言?有什么区别?

JavaScript 是一种动态类型语言。这意味着在 JavaScript 中,变量的类型在运行时确定,而不是在声明时。你可以在同一个变量中存储不同类型的值,比如一开始存储一个数字,后来存储一个字符串,而无需进行任何显式的类型转换。

主要区别:

动态类型语言(如 JavaScript)

  1. 类型灵活性:你可以在运行时改变变量的类型。这增加了编程的灵活性,使得代码编写更为简洁。
  2. 隐式类型转换:JavaScript 会自动在需要时进行类型转换,例如当你尝试将一个数字和一个字符串相加时,数字会被转换为字符串。
  3. 运行时类型检查:类型错误通常在运行时抛出,这可能使得调试更为困难,尤其是在复杂的程序中。

静态类型语言(如 Java、C++)

  1. 类型明确性:变量的类型在声明时确定,并且在整个程序的运行期间都不能改变。这有助于减少类型错误。
  2. 强制类型转换:如果需要改变变量的类型,你必须显式地进行类型转换。
  3. 编译时类型检查:类型错误通常在编译时就被捕获,这有助于在代码运行前发现和修复问题。

2.JS 引用方法有哪些?

1.使用script标签:
这是最常见的方法,通过在 HTML 文档的或部分中插入

<script src="path/to/your/script.js"></script>

script 标签可以放在 HTML 文档的任何位置,但通常放在标签的底部,以确保在 DOM 加载完成后再执行 JavaScript 代码。

2.使用模块导入(ES6 模块):
在支持 ES6 模块的环境中,你可以使用 import 语句来引入其他 JavaScript 文件或模块。例如:

import { functionName } from "./module.js";

这种方式允许你按需导入模块中的特定功能,有助于代码的组织和重用。

3.使用 CommonJS 模块(Node.js):
在 Node.js 环境中,你通常会使用 require 函数来引入模块。例如:

const moduleName = require("module-name");

这种方式是 Node.js 生态系统中的标准模块引入方式。

4.使用 AMD 或 UMD 模式:
AMD(Asynchronous Module Definition)和 UMD(Universal Module Definition)是两种用于在浏览器中异步加载模块的规范。AMD 使用 define 函数来定义模块,而 UMD 则是一种兼容 AMD 和 CommonJS 的模块定义方式。

5.使用 HTML 的 data-属性:
有时,你可能希望将 JavaScript 代码作为 HTML 元素的 data-属性来存储,并在需要时通过 JavaScript 来访问和执行它。例如:

<div id="myElement" data-my-script="console.log('Hello, world!')"></div>

然后,你可以使用 JavaScript 来提取并执行这个脚本:

const myElement = document.getElementById("myElement");
const myScript = myElement.getAttribute("data-my-script");
eval(myScript);

请注意,使用 eval 函数执行代码可能存在安全风险,因为它可以执行任何 JavaScript 代码。

6.使用动态 import()语法:
在支持动态导入的环境中,你可以使用 import()函数来动态地加载和执行 JavaScript 模块。这通常用于代码拆分或按需加载模块。例如:

button.addEventListener("click", (event) => {
  import("./module.js")
    .then((module) => {
      // 使用module中的功能
    })
    .catch((err) => {
      // 处理加载错误
    });
});

3.什么是文档的预解析?

在 JavaScript 中,文档的预解析(也称为预编译或预扫描)是浏览器在正式执行 JavaScript 代码之前,对代码进行的一次快速解析过程。这个过程主要是识别并标记出代码中的变量和函数声明,但不涉及具体的执行或计算。

优点 是提高了 JS 的执行效率,减少了不必要的错误和报警。
缺点 是可能会增加代码的复杂性和理解难度,需要开发者对预解析的规则有深入的了解。

4.什么是 DOM 和 BOM?

DOM,即文档对象模型,是一种编程接口,用于表示和操作 HTML 或 XML 文档的内容、结构和样式。它将文档视为一个树形结构,允许开发者通过 JavaScript 等脚本语言来动态地访问和修改文档中的元素。DOM 提供了一种标准化的方式来访问和更新文档,使得开发者能够创建出更加交互性和动态性的网页。

BOM,即浏览器对象模型,提供了与浏览器窗口及其功能进行交互的接口。它包含了一系列对象,如 window、navigator、location 和 history 等,这些对象提供了方法和属性,使得开发者可以控制浏览器的行为,如打开新窗口、获取浏览器信息、处理 URL 导航等。BOM 主要关注浏览器窗口和浏览器的整体行为,使得开发者能够更好地控制和管理浏览器的状态和功能。

综上所述,DOM 主要关注文档内容的操作和表示,而 BOM 主要关注浏览器窗口和浏览器功能的管理和控制。两者在 Web 开发中相互协作,使得开发者能够创建出功能丰富、交互性强的网页应用。通过掌握 DOM 和 BOM,开发者能够实现对网页内容的精确控制,以及更加灵活和高效的用户交互体验。

5.ES6 的特性有哪些?

以下是一些主要的 ES6 特性:

  1. 块级作用域和常量声明:通过letconst关键字,可以在块级作用域中声明变量和常量。let声明的变量只在声明的块级作用域内有效,而const声明的常量在声明后不能被重新赋值(但如果常量是一个对象或数组,其内部属性或元素是可以修改的)。这解决了传统var声明变量时存在的变量提升和重复声明等问题。
  2. 箭头函数:箭头函数提供了一种更简洁的函数定义语法,使用=>替代了传统的function关键字。箭头函数还有助于保持this上下文的正确性,它不会创建自己的this上下文,所以this值始终指向定义函数时的上下文。
  3. 默认参数值:ES6 允许在函数定义中为参数设置默认值。如果在函数调用时未提供对应参数,则使用默认值。这简化了函数调用时的参数传递过程。
  4. 扩展操作符:扩展操作符(...)可以用于数组和对象的展开,它允许你将数组或对象的元素/属性展开到新的数组或对象中。
  5. 解构赋值:解构赋值允许从数组或对象中提取值,并将这些值赋给新的变量。这大大简化了从数据结构中提取数据的过程。
  6. 类和模块:ES6 引入了类的概念,使得面向对象编程更加直观和易于组织。类具有构造函数、方法和继承等特性。同时,ES6 也提供了模块化的支持,通过exportimport关键字可以创建和使用模块,使得代码的组织、封装和复用更加方便。
  7. 模板字面量:模板字面量使用反引号(```)来定义字符串,它支持在字符串中插入变量和表达式,并且可以定义多行字符串。这大大增强了字符串处理的灵活性。
  8. 迭代器和生成器:迭代器使得遍历对象(如数组、Map、Set 等)更加容易,而生成器函数可以返回一个迭代器,用于逐步生成一系列的值。
  9. Promise 对象:Promise 是处理异步操作的一种新方式,它提供了一种更可靠、更易于理解的方式来组织和管理异步代码。

6.简述 JavaScript 中的 NaN 是什么?

在 JavaScript 中,NaN 是一个特殊的数值,表示非数字(Not-a-Number)。它是一个全局属性,通常作为一个无效或未定义的数值结果出现。

当进行数学运算失败或将非数字字符串转换为数字时,通常会得到 NaN。

例如,以下情况会产生 NaN:

  • 将非数字字符串转换为数字:parseInt(“hello”) 或 Number(“abc”);
  • 0 除以 0 或任何产生无穷大的操作:0/0 或 Infinity - Infinity;
  • 对非数字值进行数学运算:NaN + 5 或 Math.sqrt(-1);

NaN 具有一些特殊的行为:

  • 任何与 NaN 进行数学运算的结果仍然是 NaN。
  • NaN 与任何值(包括自身)进行比较,结果都是 false。
  • 使用 isNaN() 函数可以检查一个值是否为 NaN。

以下是一些示例:

console.log(NaN); // 输出: NaN
console.log(typeof NaN); // 输出: "number"

console.log(NaN + 5); // 输出: NaN
console.log(NaN - NaN); // 输出: NaN
console.log(NaN === NaN); // 输出: false

console.log(isNaN(NaN)); // 输出: true
console.log(isNaN("hello")); // 输出: true
console.log(isNaN(123)); // 输出: false

NaN 是一个特殊的数值,与任何其他值进行比较都不会相等,包括它本身。因此,要使用 isNaN() 函数来检查一个值是否为 NaN,而不是使用相等运算符。

7.null 和 undefined 区别?

在 JavaScript 中,nullundefined都表示某种形式的“无”或“没有值”,但它们之间存在一些关键区别。

null

  • null表示一个空的值,即该处不应该有值。它是 JavaScript 中的一个字面量,表示一个空对象引用。换句话说,null值表示一个对象预期存在,但实际上并不存在。
  • null常常用作函数的参数,表示该函数的参数不是对象。它也可以用作对象原型链的终点。
  • 当一个对象被显式地设置为null时,它表示该对象不再指向任何有效的内存地址。

undefined

  • undefined表示一个变量已被声明,但尚未被赋值。它是一个原始值,用来表示变量处于初始状态,尚未被赋予任何值。
  • 当尝试读取一个未声明的变量,或者访问一个对象上不存在的属性,或者函数定义了形参但没有传递实参时,都会返回undefined
  • undefined还用于表示函数没有返回值时的默认返回值,或者变量被声明但未初始化时的状态。

主要区别

  1. 语义和用途null表示一个对象预期存在但实际上是空的;而undefined表示一个变量或属性已声明但未定义或未赋值。
  2. 类型转换:当null被转换为数值时,它的值为 0;而undefined被转换为数值时,其值为 NaN(Not a Number)。
  3. 检测:在比较nullundefined时,建议使用严格相等运算符===,因为使用抽象相等运算符==时,nullundefined会相互等价。但严格相等运算符可以区分它们。

8.JavaScript 中有哪些数据类型,它们的区别?

JavaScript 共有 8 种数据类型:String、Number、Boolean、Object、Function、Null、Symbol、BigInt

区别:
主要区别体现在它们所表示的值和它们在内存中的存储方式:
原始类型:String、Number、Boolean、Null、Undefined 和 Symbol 是原始数据类型。它们的值是不可变的,当你试图去改变一个原始对象类型的值时,实际上是在创建一个新的原始类型的值。原始类型的值直接存储在栈内存中。
引用类型:Object、Function 和 BigInt 是引用类型。它们的值是可以改变的,并且存储的是对实际数据(堆内存中的对象)的引用。当你创建一个对象时,JavaScript 引擎会在堆内存中为该对象分配空间,并在栈内存中保存一个指向该对象的引用。

9.JavaScript 数据类型检测的方式有哪些?

typeof 运算符

let num = 123;
console.log(typeof num); // 'number'
let str = "hello";
console.log(typeof str); // 'string'
let bool = true;
console.log(typeof bool); // boolean
let obj = {};
console.log(typeof obj); // 'object'
let arr = [];
console.log(typeof arr); // 'object' 注意:数组在JavaScript中也是对象
let date = new Date();
console.log(typeof date); // object
function fun() {}
console.log(typeof fun); // 'function'
let undef;
console.log(typeof undef); // "undefined"
let nullVar = null;
console.log(typeof nullVar); //'object' 注意:null在JavaScript中也是对象,这是一个已知的错误

优点:

  • 简单易用,是 JavaScript 中检测数据类型的常见方法;
  • 对于基本类型数据的判断较为准确;

缺点:

  • 对于引用类型的判断不够准确,例如数组和 null 都会返回’object’;
  • 无法检测函数类型,会返回’functiuon’

instanceof 运算符

console.log(1 instanceof Number); // false
console.log("hello" instanceof String); // false
console.log(true instanceof Boolean); // false
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(new Date() instanceof Date); // true
console.log(function () {} instanceof Function); // true

优点:

  • 能够区分 Array、Object 和 Function,适合用于判断自定义的类实例对象

缺点:

  • 不能用来判断基本类型的数据类型;
  • 在涉及多个 iframe 或 window 对象时,可能会出现不准确的判断,因为 instanceof 检测的是对象的原型链,而不同 iframe 或 window 中的对象有不同的原型链;

constructor 属性

let num = 123;
console.log(num.constructor === Number); // true
let str = "hello";
console.log(str.constructor === String); // true
let bool = true;
console.log(bool.constructor === Boolean); // true
let arr = [];
console.log(arr.constructor === Array); // true
let obj = {};
console.log(obj.constructor === Object); // true
let date = new Date();
console.log(date.constructor === Date); // true
let fun = function(){};
console.log(fun.constructor === Function); // true
`注意`:null和undefined没有constructor属性。尝试访问null.constructor或undefined.constructor会导致错误或返回undefined。因此不能用来判断nullundefined

优点:

  • 可以直接访问创建对象的构造函数,从而判断对象的类型;

缺点:

  • constructor 属性可以被修改,因此其可靠性不如其他方法;
  • 同样不能用来判断基本类型的数据类型;

Object.prototype.toString.call() 方法

let num = 123;
console.log(Object.prototype.toString.call(num)); // "[object Number]"
let str = "hello";
console.log(Object.prototype.toString.call(str)); // "[object String]"
let bool = true;
console.log(Object.prototype.toString.call(bool)); // "[object Boolean]"
let arr = [];
console.log(Object.prototype.toString.call(arr)); // "[object Array]"
let obj = {};
console.log(Object.prototype.toString.call(obj)); // "[object Object]"
let date = new Date();
console.log(Object.prototype.toString.call(date)); // "[object Date]"
let fun = function () {};
console.log(Object.prototype.toString.call(fun)); // "[object Function]"
let nullVar = null;
console.log(Object.prototype.toString.call(nullVar)); // "[object Null]")
let undef;
console.log(Object.prototype.toString.call(undef)); // "[object Undefined]"

优点:

  • 能够返回对象的内部[[Class]],对于所有类型(包括基本数据类型和引用类型)的判断都非常的准确;
  • 是一种可靠且广泛使用的数据类型检测方法;

缺点:

  • 相对于其他方法,代码稍微复杂一些;
  • 虽然通常可靠,但在某些极端或特殊情况下,可能会受到环境或框架的影响;

10.常用的数组方法?

JavaScript 提供了许多内置的数组方法,用于处理数组数据。以下是一些常用的数组方法:

  1. 迭代方法

    • forEach(): 对数组的每个元素执行一次提供的函数。
  2. 映射方法

    • map(): 创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
  3. 过滤方法

    • filter(): 创建一个新数组,其包含通过所提供函数实现的测试的所有元素。
  4. 查找方法

    • find(): 返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
    • findIndex(): 返回数组中满足提供的测试函数的第一个元素的索引。否则返回 -1。
    • includes(): 判断一个数组是否包含一个指定的值,根据情况,如果需要,搜索也可使用值来与 NaN 进行比较。
    • indexOf(): 返回在数组中可以找到给定元素的第一个索引,如果不存在,则返回 -1。
    • lastIndexOf(): 返回指定元素在数组中的最后一个索引,如果不存在则返回 -1。
  5. 缩减方法

    • reduce(): 对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个输出值。
  6. 修改原数组的方法

    • push(): 向数组的末尾添加一个或更多元素,并返回新的长度。
    • pop(): 删除并返回数组的最后一个元素。
    • shift(): 删除并返回数组的第一个元素。
    • unshift(): 向数组的开头添加一个或更多元素,并返回新的长度。
    • splice(): 通过删除或替换现有元素或者添加新元素来修改数组,并以数组形式返回被修改的内容。
    • sort(): 对数组的元素进行排序,并返回数组。
    • reverse(): 颠倒数组中元素的顺序,并返回该数组。
    • fill(): 用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。
  7. 其他方法

    • concat(): 用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
    • slice(): 返回一个新的数组对象,是一个由开始到结束(不包括结束)选择的、由原数组的浅拷贝构成。原始数组不会被改变。
    • join(): 把数组的所有元素放入一个字符串。元素通过指定的分隔符进行分隔。
    • toString(): 返回一个字符串,表示指定的数组及其元素。
    • flat()flatMap(): 用于将嵌套的数组“拉平”,即将多维数组转换为一维数组。

这些只是 JavaScript 数组方法的一部分,但它们是日常开发中最为常用的。理解并熟练使用这些方法,可以大大提高处理数组数据的效率。

11.说一说数组去重都有哪些方法?

  1. 使用 Set 数据结构
    Set 是一种特殊的类型,它类似于数组,但是成员的值都是唯一的,没有重复的值。利用这个特性,我们可以很容易地实现数组去重。
function uniqueArray(arr) {
  return [...new Set(arr)];
}
  1. 使用 filter 方法
    filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。
function uniqueArray(arr) {
  return arr.filter((item, index) => {
    return arr.indexOf(item) === index;
  });
}
  1. 使用 reduce 方法
    reduce() 方法对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个输出值。
function uniqueArray(arr) {
  return arr.reduce(
    (prev, cur) => (prev.includes(cur) ? prev : [...prev, cur]),
    []
  );
}
  1. 使用 Map 数据结构
    Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值)都可以作为一个键或一个值。
function uniqueArray(arr) {
  let map = new Map();
  let result = [];
  for (let i = 0; i < arr.length; i++) {
    if (!map.has(arr[i])) {
      map.set(arr[i], true);
      result.push(arr[i]);
    }
  }
  return result;
}
  1. 双重循环遍历
    这是最基础的方法,通过两层循环比较数组中的元素是否重复。
function uniqueArray(arr) {
  let newArr = [];
  for (let i = 0; i < arr.length; i++) {
    let isRepeat = false;
    for (let j = 0; j < newArr.length; j++) {
      if (arr[i] === newArr[j]) {
        isRepeat = true;
        break;
      }
    }
    if (!isRepeat) {
      newArr.push(arr[i]);
    }
  }
  return newArr;
}

12.简述 JavaScript 的 map 和 foreach 的区别?

JavaScript 中的 mapforEach 都是数组的方法,用于遍历数组并对数组中的每个元素执行某种操作。尽管它们的功能在某些方面相似,但它们之间存在一些重要的区别。

  1. 返回值
  • forEach:此方法主要用于遍历数组,对数组中的每个元素执行某个操作。forEach 没有返回值,或者说它返回 undefined。它主要用于执行某种副作用(例如,打印每个元素的值或修改外部变量的值)。
  • map:此方法也用于遍历数组,但它会返回一个新的数组,新数组中的元素是通过调用提供的函数在原数组上生成的结果。换句话说,map 会根据原数组生成一个新的数组。
  1. 用途
  • forEach:当你需要遍历数组并对每个元素执行某种操作,但不关心返回结果时,可以使用 forEach
  • map:当你需要遍历数组,并基于每个元素生成一个新的数组时,应该使用 map。例如,将数组中的每个元素乘以 2,或者将数组中的每个字符串转换为大写。
  1. 中断遍历
  • forEach:在 forEach 中,你不能使用 breakreturn 来提前退出循环。如果你需要这样的功能,可能需要考虑使用 for 循环或其他方法。
  • map:同样,map 也不支持使用 breakreturn 来提前退出循环。
  1. 链式调用
  • 由于 map 返回一个新的数组,因此它可以与其他数组方法(如 filterreduce 等)进行链式调用,从而创建一个处理数据的流水线。而 forEach 由于没有返回值,因此不能用于链式调用。

总的来说,选择使用 map 还是 forEach 取决于你的具体需求。如果你需要生成一个新的数组,那么应该使用 map。如果你只是需要遍历数组并执行某种操作,而不关心返回值,那么可以使用 forEach

13.简述在 Javascript 中什么是伪数组?如何将伪数组转化为标准数组?

伪数组指的是具有 length 属性和按索引访问元素能力的对象,但它并不是真正的数组。伪数组没有数组的内置方法,例如 push()、pop()、forEach() 等。常见的伪数组包括函数的 arguments 对象和 jQuery 选择器返回的对象。

要将伪数组转化为标准数组,可以使用以下几种方法:

使用 Array.prototype.slice.call(): 利用了 call 方法来改变 slice 方法的上下文,使其作用于伪数组。

var arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
var realArray = Array.prototype.slice.call(arrayLike);
console.log(realArray); // 输出: ['a', 'b', 'c']

使用扩展运算符(…): 可以将伪数组转化为标准数组。

var arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
var realArray = [...arrayLike];
console.log(realArray); // 输出: ['a', 'b', 'c']

使用 Array.from(): 方法也可以将伪数组转化为标准数组。

var arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
var realArray = Array.from(arrayLike);
console.log(realArray); // 输出: ['a', 'b', 'c']

14.常用的字符串方法?

在 JavaScript 中,字符串对象提供了一系列非常有用的方法,用于处理和操作字符串。以下是一些常用的字符串方法:

  1. charAt(index):返回指定索引位置的字符。如果索引超出范围,则返回空字符串。
const str = "hello";
console.log(str.charAt(1)); // 输出 'e'
  1. charCodeAt(index):返回指定索引位置的字符的 Unicode 编码。
const str = "hello";
console.log(str.charCodeAt(1)); // 输出 101 (字符 'e' 的Unicode编码)
  1. concat(string2, string3, ..., stringX):用于连接两个或多个字符串,并返回新的字符串。
const str1 = "Hello, ";
const str2 = "World!";
console.log(str1.concat(str2)); // 输出 'Hello, World!'
  1. indexOf(searchValue[, fromIndex]):返回指定值在字符串中首次出现的索引,如果未找到则返回-1。
const str = "hello world";
console.log(str.indexOf("world")); // 输出 6
  1. lastIndexOf(searchValue[, fromIndex]):返回指定值在字符串中最后一次出现的索引,如果未找到则返回-1。
const str = "hello world, hello universe";
console.log(str.lastIndexOf("hello")); // 输出 13
  1. slice(startIndex[, endIndex]):提取字符串的某个部分,并返回新的字符串(原字符串不变)。
const str = "Hello, World!";
console.log(str.slice(0, 5)); // 输出 'Hello'
  1. substring(indexStart[, indexEnd]):与slice类似,但无法处理负数索引。
const str = "Hello, World!";
console.log(str.substring(7, 12)); // 输出 'World'
  1. substr(start[, length]):从指定位置开始提取指定长度的字符。
const str = "Hello, World!";
console.log(str.substr(7, 5)); // 输出 'World'
  1. toUpperCase():将字符串转换为大写。
const str = "hello";
console.log(str.toUpperCase()); // 输出 'HELLO'
  1. toLowerCase():将字符串转换为小写。
const str = "HELLO";
console.log(str.toLowerCase()); // 输出 'hello'
  1. trim():移除字符串两端的空白符。
const str = "   hello world   ";
console.log(str.trim()); // 输出 'hello world'
  1. replace(regexp|substr, newSubstr|function):替换与正则表达式匹配的子串,或替换与指定子串匹配的子串。
const str = "Hello, World!";
console.log(str.replace("World", "Universe")); // 输出 'Hello, Universe!'
  1. split(separator[, limit]):使用指定的分隔符将字符串分割成子串数组。
const str = "apple,banana,orange";
console.log(str.split(",")); // 输出 ['apple', 'banana', 'orange']
  1. startsWith(searchString[, position]):检测字符串是否以指定的子串开始。
const str = "Hello, World!";
console.log(str.startsWith("Hello")); // 输出 true
  1. endsWith(searchString[, position]):检测字符串是否以指定的子串结束。
const str = "Hello, World!";
console.log(str.endsWith("World!")); // 输出 true
  1. includes(searchString[, position]):判断字符串是否包含指定的子串。
const str = "Hello, World!";
console.log(str.includes("World")); // 输出 true
  1. repeat(count):返回一个新字符串,该字符串包含指定数量的字符串的副本。
const str = "hello";
console.log(str.repeat(3)); // 输出 'hellohellohello'

15.JavaScript 中的 split、slice、splice 函数区别?

主要区别:

  1. split()
    split() 是字符串对象的一个方法,用于将字符串分割成子字符串数组,并返回这个新数组。这个方法基于指定的分隔符来分割字符串。
const str = "Hello,World,How,Are,You";
const arr = str.split(","); // ["Hello", "World", "How", "Are", "You"]
  1. slice()
    slice() 是数组和字符串对象的一个方法,用于提取某个范围内的元素或字符,并返回这些元素或字符组成的新数组或字符串。原数组或字符串不会被改变。
// 对于数组:
const arr = [1, 2, 3, 4, 5];
const newArr = arr.slice(1, 3); // [2, 3]
// 对于字符串:
const str = "Hello,World";
const newStr = str.slice(0, 5); // "Hello"
  1. splice()
    splice() 是数组对象的一个方法,用于在任意的位置给数组添加/删除任意个元素,并返回被删除的元素组成的数组。原数组会被改变。
const arr = [1, 2, 3, 4, 5];
const deletedArr = arr.splice(1, 2, "a", "b"); // [2, 3]
console.log(arr); // [1, "a", "b", 4, 5]

总结

  • split() 是字符串的方法,用于将字符串分割成数组。
  • slice() 是数组和字符串的方法,用于提取子数组或子字符串,不改变原数组或字符串。
  • splice() 是数组的方法,用于添加/删除元素,并返回被删除的元素,改变原数组。

16.for…in 和 for…of 的区别

  1. 遍历的数据结构类型:for…in 循环主要是为遍历对象而生,它遍历的是对象的属性名称,包括对象本身的属性和其原型链上的属性。因此,使用 for…in 可能会遍历到预期外的属性,性能较差。而 for…of 循环则是 ES6 新增的遍历方式,它允许遍历一个含有 iterator 接口的数据结构(如数组、对象、类数组对象、字符串、Set、Map 以及 Generator 对象等),并且返回各项的值。for…of 只遍历当前对象,不会遍历原型链,因此性能更优。
  2. 返回值类型:for…in 循环返回的是对象的键名,无论是遍历对象还是数组,它都会返回相应的键名。而 for…of 循环返回的是键值对中的值,对于数组,它会返回数组下标对应的属性值。
  3. 与其他控制结构的配合:for…of 循环可以与 break、continue 和 return 配合使用,使得在遍历过程中可以随时退出循环。而 for…in 循环则没有这样的特性。

17.Object.is() 与比较操作符 “===”、“= =” 的区别?

JavaScript 中的Object.is()方法和比较操作符===(严格相等)以及==(抽象相等)在比较两个值时存在一些重要的区别。

  1. ===(严格相等)

===操作符比较两个值是否严格相等。这意味着它会比较值和类型。如果两个操作数都是相同的类型并且具有相同的值,那么===将返回true,否则返回false

例如:

0 === 0; // true
0 === "0"; // false,因为类型不同
  1. == (抽象相等)

==操作符在比较时会进行类型转换。如果两个操作数不是同一类型,JavaScript 会尝试将它们转换为相同的类型,然后再进行比较。这通常会导致一些不易预测的结果,因此许多开发者倾向于避免使用==,并坚持使用===

例如:

0 == "0"; // true,因为JavaScript将字符串'0'转换为数字0进行比较
  1. Object.is()
    Object.is()方法用于比较两个值是否完全相同,它与===操作符几乎相同,但在处理两个特殊的值时有所不同:NaN-0
  • 对于NaNObject.is(NaN, NaN)会返回true,而NaN === NaN会返回false。这是因为NaN是一个特殊的值,它不等于任何值,包括它自己。但是,Object.is()提供了一个方法来识别两个NaN值是否相等。
  • 对于-0+0Object.is(-0, +0)会返回false,而-0 === +0会返回true。这是因为虽然-0+0在数值上是相等的,但它们在数学上是不同的,Object.is()能够识别这种区别。

例如:

Object.is(NaN, NaN); // true
Object.is(-0, +0); // false

总的来说,Object.is()提供了比===更严格的相等性检查,特别是在处理特殊值如NaN-0时。在大多数情况下,使用===进行值比较是足够的,但在需要处理这些特殊值的情况下,Object.is()可能会更有用。而==由于其类型转换的特性,可能会导致不可预测的结果,因此应尽量避免使用。

18.三种强制类型转换和两种隐式类型转换?

强制类型转换

  1. Number():这个方法可以将几乎任何类型的值转换为数字。例如,对于字符串,它会尝试解析字符串的开始部分,直到遇到不能转换为数字的字符为止。对于布尔值,true会被转换为 1,false会被转换为 0。对于对象,如果对象有一个名为valueOf的方法,该方法会被调用并返回结果。如果valueOf返回的不是原始值,那么toString方法会被调用。
Number("123"); // 123
Number(true); // 1
Number(false); // 0
  1. String():这个方法可以将任何类型的值转换为字符串。对于数字,它会返回数字的文本形式。对于布尔值,truefalse会分别转换为字符串"true"和"false"。对于对象,如果对象有一个名为toString的方法,该方法会被调用并返回结果。
String(123); // "123"
String(true); // "true"
String(false); // "false"
  1. Boolean():这个方法可以将任何类型的值转换为布尔值。以下值会被转换为falsefalse0""(空字符串)、nullundefinedNaN。其他所有值都会被转换为true
Boolean(0); // false
Boolean(""); // false
Boolean(null); // false
Boolean(undefined); // false
Boolean(NaN); // false

隐式类型转换

  1. 加法运算符(+):当加法运算符的一个操作数是字符串时,另一个操作数会被转换为字符串,然后执行字符串连接。如果两个操作数都是数字,那么执行数字加法。
1 + "2"; // "12"
"1" + 2; // "12"
  1. 比较运算符(== 和 ===):在 JavaScript 中,=====是两个不同的比较运算符。===是严格相等运算符,它不会进行类型转换。而==会进行类型转换,以便比较不同类型的值。例如,当使用==比较一个字符串和一个数字时,字符串会被转换为数字,然后进行比较。
"1" == 1; // true,因为'1'被转换为数字1
"1" === 1; // false,因为类型不同,不进行类型转换

19.var、let、const 的区别?

varletconst是 JavaScript 中用于声明变量的关键字,它们之间有一些关键的区别,主要体现在作用域、重复声明、重新赋值以及初始化等方面。

  1. 作用域

    • var声明的变量具有函数作用域或全局作用域,取决于它在哪里被声明。如果在函数内部声明,则作用域为该函数;如果在全局上下文中声明,则作用域为全局。
    • letconst声明的变量具有块级作用域,即它们的作用域被限制在声明它们的块(大括号 {})内,无论是循环、条件语句还是任何其他块。
  2. 重复声明

    • var允许在相同的作用域内重复声明同一个变量,但这样做会导致变量值被重新赋值,而不是报错。
    • let不允许在相同的作用域内重复声明同一个变量。如果尝试这样做,JavaScript 会抛出一个错误。
    • const也不允许重复声明。此外,由于const声明的变量是只读的,一旦赋值后就不能再改变其指向的值(但如果是对象或数组,其内部属性或元素是可以修改的)。
  3. 重新赋值

    • varlet声明的变量都可以被重新赋值。
    • const声明的变量则不能重新赋值。一旦赋值后,就不能再改变其指向的值。
  4. 初始化

    • var声明的变量如果不初始化,会被赋予undefined的值。
    • let声明的变量如果不初始化,同样会被赋予undefined的值,但如果在引用前未初始化,使用前会被暂时提升(hoisting),但此时其值为undefined
    • const声明的变量必须在声明时立即初始化,否则会导致语法错误。

综上所述,varletconst的主要区别在于它们的作用域、重复声明、重新赋值以及初始化的行为。在编写 JavaScript 代码时,根据具体的需求和上下文,选择适当的声明关键字是很重要的。

20.解释 JavaScript 中的“作用域”是什么意思?

在 JavaScript 中,“作用域”(Scope)是指变量、函数和对象的可访问性。换句话说,它定义了变量、函数和对象的可见性范围。在 JavaScript 中,作用域基本上是确定代码块中变量、函数和对象的生命周期和可见性的规则。
JavaScript 有两种基本的作用域:

全局作用域: 在代码的任何位置都可以访问全局变量。当你在所有函数外部声明一个变量时,该变量就是全局变量。

var globalVar = "I am global";

function myFunction() {
  console.log(globalVar); // 可以访问全局变量
}
myFunction(); // 输出 "I am global"

局部作用域: 局部作用域也称为函数作用域,因为它是在函数内部定义的。只有在该函数内部才能访问局部变量。

function myFunction() {
  var localVar = "I am local";
  console.log(localVar); // 可以访问局部变量
}
myFunction(); // 输出 "I am local"
console.log(localVar); // ReferenceError: localVar is not defined,因为 localVar 只在 myFunction 的作用域内存在

除了全局作用域和局部作用域,ES6 还引入了块级作用域的概念,主要通过 let 和 const 关键字实现。块级作用域是在一对花括号 {} 内定义的,在这对花括号之外就不能访问这个作用域内定义的变量。

if (true) {
  let blockVar = "I am block scoped";
  console.log(blockVar); // 可以访问 blockVar
}
console.log(blockVar); // ReferenceError: blockVar is not defined,因为 blockVar 只在 if 语句的块级作用域内存在

21.变量提升是什么?

JavaScript 中的变量提升是指在代码执行前,JavaScript 引擎会将变量的声明(而非其赋值)移动到它们所在作用域的最顶部。这意味着即使变量在代码中出现在后面,其声明在逻辑上也被视为在作用域的开始处。然而,变量的赋值操作不会被提升,所以在声明和赋值之间引用变量会得到 undefined。

22.什么是暂时性死区?

在 JavaScript 中,"暂时性死区"是 ES6 引入 let 和 const 关键字后所引入的一个概念。在 ES5 及之前的版本中,我们使用 var 关键字来声明变量。var 声明的变量存在变量提升的现象,即无论变量在何处声明,它都会被提升到其所在作用域的最顶部。但是,变量的赋值操作并不会被提升,因此如果在声明之前就访问这个变量,它的值是 undefined。然而,在 ES6 中,let 和 const 声明的变量不存在变量提升,而是进入了暂时性死区。这意味着在声明之前的任何代码都不能访问这个变量,如果尝试访问,JavaScript 引擎会抛出一个引用错误。

例如:

console.log(x); // ReferenceError: x is not defined
let x = "hello";

在这个例子中,变量 x 在声明之前就被访问了,所以抛出了一个引用错误。在变量 x 被声明之前的区域,就是 x 的暂时性死区。

总结:

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

23.Javascript中 什么是未声明变量?什么是未定义?有什么区别?

未声明变量(Undeclared Variables)
未声明变量指的是在代码中使用了一个从未声明过的变量。在 JavaScript 中,如果你尝试访问一个未声明的变量,将会导致一个ReferenceError。这是因为 JavaScript 引擎在当前的执行上下文中找不到这个变量的声明。
例如:

console.log(myVariable); // 抛出 ReferenceError: myVariable is not defined

在这个例子中,myVariable从未被声明,因此当尝试打印其值时,JavaScript 引擎会抛出一个错误。

未定义变量(Undefined Variables)
未定义变量是指已经声明但未被赋值的变量。在 JavaScript 中,声明但未初始化的变量会被自动赋予undefined值。未定义变量不会导致运行时错误,但如果你尝试使用它们的值(比如进行数学运算),可能会得到不期望的结果。
例如:

var anotherVariable;
console.log(anotherVariable); // 输出 undefined

在这个例子中,anotherVariable被声明了,但由于没有赋值,所以它的值是undefined。尝试访问它的值不会导致错误,但你会得到undefined

区别

  1. 错误类型:未声明变量在尝试访问时会抛出ReferenceError,而未定义变量则不会抛出错误,其值是undefined

  2. 存在性:未声明变量在当前的执行上下文中是不存在的,而未定义变量是存在的,只是它的值是undefined

  3. 作用域:未声明的变量可能会导致全局作用域污染,因为它们可能意外地成为全局变量。而未定义变量则只在其声明的作用域内有效。

  4. 可预测性:未定义变量通常比未声明变量更可预测,因为你可以明确地知道变量的存在,只是它的值未知(即undefined)。而未声明变量则可能导致程序突然中断,因为访问它们会抛出错误。

24.说说 JavaScript 中 new 操作符具体干了什么?

在 JavaScript 中,new操作符用于创建一个用户自定义的对象类型的实例或具有构造函数的内置对象的实例。以下是new操作符在 JavaScript 中执行的主要步骤:

  1. 创建一个新的空对象:首先,new操作符会创建一个新的空对象。这个对象会继承自构造函数的prototype对象。
  2. 设置原型链:新创建的对象的__proto__属性(也就是其内部原型)会被设置为构造函数的prototype对象。这使得新对象能够访问构造函数prototype对象上的所有属性和方法。
  3. 将构造函数的作用域赋给新对象(即this指向新对象):然后,new操作符会将构造函数的this关键字绑定到新创建的对象上。这样,构造函数中定义的任何属性和方法都会添加到新对象上。
  4. 如果构造函数返回非对象值,则返回新对象:如果构造函数没有显式地返回一个对象,那么new操作符会返回新创建的对象。如果构造函数返回了一个对象,那么new操作符会返回这个对象,而不是新创建的对象。这提供了一种可以覆盖new操作符默认行为的机制。

25.对象的几种创建方式?

在 JavaScript 中,有多种创建对象的方式。以下是其中的一些主要方法:

  1. 字面量方式
    这是创建对象的最直接方式,它使用花括号 {} 来定义对象的属性和方法。
let obj = {
  name: "John",
  age: 30,
  sayHello: function () {
    console.log("Hello, my name is " + this.name);
  },
};
  1. 构造函数方式
    构造函数是一个特殊的函数,用于初始化新创建的对象。JavaScript 中的构造函数通常以大写字母开头,以区别于其他函数。
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayHello = function () {
    console.log("Hello, my name is " + this.name);
  };
}

let person = new Person("John", 30);
  1. Object.create() 方法
    Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的proto
let personProto = {
  sayHello: function () {
    console.log("Hello, my name is " + this.name);
  },
};

let person = Object.create(personProto);
person.name = "John";
person.age = 30;
  1. class 语法(ES6)
    在 ES6 中,引入了 class 关键字,使得对象的创建更加面向对象和易于理解。然而,需要注意的是,JavaScript 中的 class 实际上只是语法糖,它的底层仍然是基于原型链的。
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log("Hello, my name is " + this.name);
  }
}

let person = new Person("John", 30);

26.面向对象的特性有哪些,及三大特点?

面向对象的三大特性是封装继承多态

封装:封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
继承:继承就是让一个类型的对象拥有另一个类型的对象的属性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。
多态:多态性是指允许不同类的对象对同一消息作出响应,同一个类型的对象在执行同一个方法时,可以表现出多种行为特征。

27.怎么判断某个对象是否包含指定成员?

  1. 使用 hasOwnProperty 方法: 这个方法会检查对象自身(不包括原型链)是否有指定的属性。
let obj = { property: "value" };
console.log(obj.hasOwnProperty("property")); // 输出:true
console.log(obj.hasOwnProperty("nonExistent")); // 输出:false
  1. 使用 in 操作符: 这个操作符会检查对象自身以及它的原型链中是否有指定的属性。
let obj = { property: "value" };
console.log("property" in obj); // 输出:true
console.log("nonExistent" in obj); // 输出:false
  1. 直接访问属性: 如果属性存在,访问它不会返回 undefined。然而,需要注意的是,如果一个属性的值是 undefined,这种方法也会返回 true。
let obj = { property: "value" };
console.log(obj.property !== undefined); // 输出:true
console.log(obj.nonExistent !== undefined); // 输出:true,但这里是有问题的,因为nonExistent属性不存在,访问它会返回undefined

在大多数情况下,使用 hasOwnProperty 或 in 操作符是更安全和更准确的。特别是当您想要检查对象自身是否有某个属性,而不是检查原型链时,hasOwnProperty 是一个好选择。如果您想要检查对象自身或原型链中是否有某个属性,那么可以使用 in 操作符。

28.JavaScript 的数据对象有哪些属性值?

  1. 数值(Number):可以是整数或浮点数。
let obj = {
  age: 30,
  price: 19.99,
};
  1. 字符串(String):文本值。
let obj = {
  name: "John Doe",
  email: "johndoe@example.com",
};
  1. 布尔值(Boolean)truefalse
let obj = {
  isAdmin: true,
  hasAccess: false,
};
  1. 对象(Object):可以是另一个对象字面量、数组、函数等。
let obj = {
  address: {
    street: "123 Main St",
    city: "Anytown",
    state: "CA",
  },
  hobbies: ["reading", "hiking", "coding"],
};
  1. 数组(Array):有序的元素集合。
let obj = {
  scores: [90, 85, 88, 92],
};
  1. 函数(Function):可以作为对象的方法。
let obj = {
  greet: function () {
    console.log("Hello, my name is " + this.name);
  },
};
  1. null:表示一个空值或“无”的值。
let obj = {
  nothingHere: null,
};
  1. undefined:表示变量未定义或没有赋值。
let obj = {
  somethingMissing: undefined,
};
  1. Symbol:唯一且不可变的数据类型,通常用作对象的属性键。
let sym = Symbol("uniqueKey");
let obj = {
  [sym]: "unique value",
};
  1. BigInt:可以表示任意大的整数。
let obj = {
  bigNumber: BigInt("9007199254740991"),
};

29.JavaScript 宿主对象和原生对象的区别?

  1. 定义 原生对象是独立于宿主环境之外的对象,包括 Object、Array、Function、Number、String、Date 等。而宿主对象则是 JavaScript 引擎在运行过程中,由 JavaScript 宿主环境(如浏览器或 Node.js)通过某种机制注入到 JavaScript 引擎中的对象,例如浏览器的 BOM(Browser Object Model)和 DOM(Document Object Model)对象。
  2. 创建方式: 原生对象包括内置对象(由 ECMAScript 提供并独立于宿主对象之外的对象)和 JavaScript 运行过程中动态创建的对象。而宿主对象是由 JavaScript 宿主环境提供的,不是由 JavaScript 代码直接创建的。
  3. 包含内容: 原生对象主要包含一些基础的数据类型和对象,如 Object、Array、Function、Number、String、Date 等。而宿主对象则包含了与特定宿主环境相关的对象和方法,例如浏览器的 window 对象、document 对象等。

总的来说,原生对象是独立于宿主环境之外的基础对象,而宿主对象则是与特定宿主环境相关的对象。在 JavaScript 中,原生对象和宿主对象共同构成了完整的 JavaScript 运行环境。

30.对 AJAX 的理解,实现一个 AJAX 请求?

AJAX,全称 Asynchronous JavaScript and XML,是一种创建交互式网页应用的网页开发技术。AJAX 使用 JavaScript 语言向服务器提出请求并处理响应而不阻塞用户。在这个过程中,网页不需要重新加载,只更新需要改变的部分。

实现一个 AJAX 请求通常涉及以下步骤:

  1. 创建 XMLHttpRequest 对象:这是发起 AJAX 请求的基础。
  2. 设置请求方法和 URL:使用open()方法设置 HTTP 请求方法和请求的 URL。
  3. 设置请求头(如果需要):使用setRequestHeader()方法设置请求头。
  4. 发送请求:使用send()方法发送请求。对于 GET 请求,通常不需要传递任何参数给send();对于 POST 请求,需要将要发送的数据作为send()的参数。
  5. 处理响应:通过监听onreadystatechange事件或使用 Promise 来处理响应。当readyState为 4 且status为 200 时,表示请求成功完成,可以处理响应数据。

以下是一个简单的 AJAX GET 请求示例:

// 1. 创建XMLHttpRequest对象
var xhr = new XMLHttpRequest();
// 2. 设置请求方法和URL
xhr.open("GET", "https://api.example.com/data", true); // 第三个参数true表示异步请求
// 3. 发送请求
xhr.send();
// 4. 处理响应
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    // 请求成功完成,处理响应数据
    var responseData = JSON.parse(xhr.responseText);
    console.log(responseData);
  }
};

31.ajax、axios、fetch 的区别?

  1. ajax: 是指一种创建交互式网页应用的网页开发技术,并且可以做到无需重新加载整个网页的情况下,能够更新部分网页,也叫作局部更新
    优点:

    • 局部更新
    • 原生支持,不需要任何插件
    • 原生支持,不需要任何插件
      缺点:
    • 可能破坏浏览器后退功能
    • 嵌套回调,难以处理
  2. axios: 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中
    优点:

    • Axios 既可以在浏览器中运行,也可以在 node.js 环境中使用
    • 支持 Promise API
    • 拦截请求和响应
    • 转换请求数据和响应数据
    • 取消请求
    • 自动转换 JSON 数据
    • 客户端支持防御 XSRF
      缺点:
    • 会增加项目的依赖项
    • 对就得浏览器有兼容性问题
  3. fetch: 使用了 ES6 中的 promise 对象。Fetch 是基于 promise 设计的。Fetch 函数就是原生 js,没有使用 XMLHttpRequest 对象。

优点:

  • 更加底层,提供的 API 丰富(request, response)
  • 脱离了 XHR,是 ES 规范里新的实现方式

缺点:

  • fetch 是一个低层次的 API,你可以把它考虑成原生的 XHR,所以使用起来并不是那么舒服,需要进行封装
  • fetch 只对网络请求报错,对 400,500 都当做成功的请求,需要封装去处理
  • fetch 默认不会带 cookie,需要添加配置项
  • fetch 不支持 abort,不支持超时控制,使用 setTimeout 及 Promise.reject 的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
  • fetch 没有办法原生监测请求的进度,而 XHR 可以

32.请说说 post 请求与 get 请求有什么不同?

  1. 请求参数的位置
    • GET 请求的参数通常附加在 URL 的后面,使用问号(?)开始,并用&符号分隔参数。因此,GET 请求的数据会暴露在 URL 中。
    • POST 请求的参数则包含在请求体中,不会在 URL 中显示。这意味着 POST 请求可以发送大量的数据,且数据不会显示在 URL 中。
  2. 安全性
    • GET 由于请求的参数在 URL 中可见,因此它不适合发送敏感信息,如密码或私人数据。这些信息可能会被保存在浏览器的历史记录、网络日志或服务器日志中,存在安全风险。
    • POST 请求的参数在请求体中,因此相对更安全,适合发送敏感数据。然而,它并非完全安全,仍需要采取其他安全措施,如 HTTPS,来确保数据传输的安全性。
  3. 数据大小限制
    • GET 请求由于将参数附加在 URL 中,因此受到 URL 长度的限制。不同的浏览器和服务器对 URL 长度有不同的限制,但通常不建议在 GET 请求中发送大量数据。
    • POST 请求没有这样的限制,因为它将参数放在请求体中。因此,POST 请求可以发送比 GET 请求更多的数据。
  4. 幂等性
    • GET 请求是幂等的,这意味着多次执行相同的 GET 请求,对服务器上的资源没有影响。
    • POST 请求通常不是幂等的。每次发送 POST 请求都可能在服务器上创建新的资源或修改现有资源。
  5. 缓存
    • GET 请求可以被缓存,因此如果相同的 GET 请求被多次发送,可能直接从缓存中获取响应,而不必每次都从服务器获取。
    • POST 请求通常不会被缓存。
  6. 后退/刷新按钮的影响
    • 由于GET 请求的结果可以被缓存,因此用户可以安全地使用浏览器的后退和刷新按钮。
    • 对于POST请求,由于可能涉及数据的创建或修改,使用后退和刷新按钮可能会导致不可预测的结果。
  7. 用途
    • GET请求通常用于从服务器检索数据,如查询数据库或获取页面内容。
    • POST 请求通常用于向服务器提交数据,如提交表单或上传文件。

33. 说一说 promise 是什么与使用方法?

Promise 是一种用于处理异步操作的对象,它代表了某个在未来才会知道结果的事件(通常是一个异步操作)。Promise 的主要作用是将异步操作队列化,并按照期望的顺序执行,返回符合预期的结果。通过 Promise,我们可以更好地管理代码的执行顺序,并避免回调地狱的问题。

原理:
Promise 基于状态机和事件触发。每个 Promise 对象都有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。Promise 的状态一旦改变就不会再变,只能从 pending 变为 fulfilled 或 rejected。这种不可逆性确保了 Promise 对象的稳定性。
使用方法:

  1. 创建 Promise 对象:通过 new Promise() 构造函数来创建一个新的 Promise 实例。构造函数接收一个函数作为参数,这个函数有两个参数:resolvereject,分别用于处理异步操作成功和失败的情况。
    示例:
let promise = new Promise(function(resolve, reject) {
    // 异步操作
    setTimeout(() => {
        if (/* 异步操作成功 */) {
            resolve('操作成功');
        } else {
            reject('操作失败');
        }
    }, 1000);
});
  1. 处理 Promise 结果:使用 .then() 方法来处理 Promise 对象的状态变化。.then() 方法接收两个函数作为参数,第一个函数是 Promise 状态变为 fulfilled 时调用的回调函数,第二个函数(可选)是 Promise 状态变为 rejected 时调用的回调函数。
    示例:
promise.then(
  function (result) {
    // 异步操作成功时的处理逻辑
    console.log(result); // 输出:操作成功
  },
  function (error) {
    // 异步操作失败时的处理逻辑
    console.log(error); // 输出:操作失败
  }
);

或者,如果只需要处理成功的状态,可以只提供一个函数作为 .then() 方法的参数。如果只想处理错误状态,可以使用 .catch() 方法。
示例(简化版):

promise
  .then((result) => console.log(result)) // 处理成功的情况
  .catch((error) => console.log(error)); // 处理失败的情况
  1. Promise 链式调用:由于 .then() 方法返回一个新的 Promise,因此可以链式调用多个 .then() 来处理异步操作的结果。这有助于将多个异步操作按照顺序连接起来。
    示例:
promise
  .then((result1) => {
    // 处理第一个异步操作的结果
    return anotherAsyncOperation(result1); // 返回一个新的 Promise
  })
  .then((result2) => {
    // 处理第二个异步操作的结果
    console.log(result2);
  })
  .catch((error) => {
    // 处理任何一个异步操作中的错误
    console.log(error);
  });

Promise 的这些特性使得异步编程更加简洁和直观,减少了嵌套回调的使用,提高了代码的可读性和可维护性。同时,Promise 还提供了诸如 Promise.all()Promise.race() 等静态方法,用于处理多个 Promise 对象的情况。

34.promise.all 的作用、优缺点和用法?

Promise.all 是 JavaScript 中的一个方法,它在处理多个异步操作时非常有用。其作用是将多个 Promise 实例包装成一个新的 Promise 实例,并且这个新的 Promise 的状态由所有的子 Promise 决定。
优点:

  1. 简化异步处理:Promise.all 使得处理多个异步操作变得更为简单和直观,特别是当需要等待所有异步操作都完成时。
  2. 结果顺序一致性:Promise.all 保证了子 Promise 对象结果的顺序与原始数组中的顺序一致,这对于需要按特定顺序处理异步操作结果的场景非常有用。
  3. 错误捕获:通过 Promise.all,可以方便地捕获任何一个子 Promise 对象的错误,并在一个统一的错误处理函数中处理它们。

缺点:

  1. 一荣俱荣,一损俱损:Promise.all 的机制是“一荣俱荣,一损俱损”,即只要有一个 Promise 失败,整个 Promise.all 就会失败。在某些场景下,可能希望即使部分 Promise 失败,也能继续处理其他成功的 Promise,这时 Promise.all 可能不是最佳选择。
  2. 性能考虑:如果子 Promise 对象的数量非常大,Promise.all 可能会占用较多的内存和处理时间,因为需要等待所有 Promise 完成并存储它们的结果。

用法:

  1. 输入:Promise.all 接受一个包含多个 Promise 对象的数组作为参数。这个数组可以包含任何类型的值,但只有 Promise 对象的状态变化(即 fulfilled 或 rejected)才会影响 Promise.all 返回的新的 Promise 对象的状态。
  2. 状态变化:当所有的子 Promise 都成功完成(即状态变为 fulfilled)时,Promise.all 返回的新的 Promise 对象才会成功完成,并且其结果是所有子 Promise 结果的数组,顺序与原始数组中的 Promise 顺序一致。然而,如果有任何一个子 Promise 失败(即状态变为 rejected),那么 Promise.all 返回的新的 Promise 对象会立即失败,并且其结果是第一个失败的子 Promise 的结果。
  3. 使用场景:Promise.all 特别适用于需要等待多个异步操作全部完成,并且需要获取所有异步操作结果的场景。例如,在 Web 开发中,可能需要从多个 API 端点获取数据,并在所有数据都加载完成后进行下一步操作。这时候,就可以使用 Promise.all 来等待所有的 API 请求完成,并收集所有的结果。

以下是一个简单的使用示例:

let promise1 = fetch("url1"); // 假设这是一个返回Promise的API请求
let promise2 = fetch("url2"); // 同上

Promise.all([promise1, promise2])
  .then((results) => {
    // 当两个请求都成功时,这里会被调用
    // results是一个数组,包含了两个请求的结果
    console.log(results);
  })
  .catch((error) => {
    // 当任何一个请求失败时,这里会被调用
    console.error("An error occurred:", error);
  });

在这个示例中,Promise.all 会等待两个 fetch 请求都完成,然后将它们的结果作为数组传递给.then()方法的回调函数。如果任何一个请求失败,Promise.all 会立即失败,并将错误传递给.catch()方法的回调函数。

35.JavaScript 脚本延迟加载的方式有哪些?

  1. defer 属性: 给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
  2. async 属性: 给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
  3. 动态创建 DOM 方式: 动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
  4. 使用 setTimeout 延迟方法: 设置一个定时器来延迟加载 js 脚本文件。
  5. 让 JS 最后加载: 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

36.说一说 defer 和 async 区别?

  1. 执行时间不同:defer 是表明脚本在执行时不会影响页面的构造,脚本会被延迟到整个页面都解析完毕后再运行;async 是浏览器立即异步下载文件,下载完成会立即执行,此时会阻塞 DOM 渲染。
  2. 执行顺序不同:defer 是按照顺序下载执行;async 是不能保证多个加载时的先后顺序。
  3. 用途不同:defer 是表明脚本在执行时不会影响页面的构造;async 是为了实现并行加载,加速页面渲染。

37.介绍下 Set、 Map. WeakSet 和 WeakMap 的区别?

  1. Set
    Set 是一种特殊的类型,它类似于数组,但成员的值都是唯一的,没有重复的值。Set 本身是一个构造函数,用来生成 Set 数据结构。Set 的主要方法包括 add(),delete(),has()和 clear()。
    特点:

    • 成员的值是唯一的,没有重复的值。
    • 可以遍历成员。
    • size 属性返回成员总数。
    • add(value) 方法添加某个值,返回 Set 结构本身。
    • delete(value) 方法删除某个值,返回一个布尔值,表示删除是否成功。
    • has(value) 方法返回一个布尔值,表示该值是否为 Set 的成员。
    • clear() 方法清除所有成员,没有返回值。
  2. Map
    Map类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(对象或者原始值)都可以当作键。Map 提供了许多方法来遍历和操作数据。Map 的主要方法包括 set(),get(),has(),delete()和 clear()。
    特点

    • 它类似于对象,也是键值对的集合。
    • 任何类型的值(对象或者原始值)都可以作为一个键或一个值。
    • 提供了许多方法来遍历和操作数据,如 sizeset(key, value)get(key)has(key)delete(key)clear() 等。
  3. WeakSet
    WeakSet是对象的弱集合,它允许你存储对象的弱引用,也就是说,如果没有其他引用指向这个对象,那么这个对象就会被垃圾回收机制自动回收。这是防止内存泄漏的一个非常有用的特性。WeakSet 只有 add(),delete()和 has()三个方法。
    特点

    • 成员只能是对象,而不能是其他类型的值。
    • WeakSet 中的对象都是弱引用,如果没有其他地方引用该对象,则垃圾回收机制可以回收它。
    • 由于上述的弱引用特性,WeakSet 没有 size 属性,也不允许遍历。
  4. WeakMap
    WeakMap 是一种键必须是对象的映射结构,它的键是弱引用,也就是说,如果键对象没有其他引用指向它,那么该键和对应的值就会被垃圾回收机制自动回收。这是防止内存泄漏的另一个非常有用的特性。WeakMap 只有 set(),get(),has(),delete()四个方法。
    特点

    • 键只能是对象,而值可以是任意的。
    • WeakMap 的键是弱引用,如果没有其他地方引用该对象,则垃圾回收机制可以回收它。
    • 由于上述的弱引用特性,WeakMap 没有 size 属性,也不允许遍历。
    • WeakMap 只有两个方法:get(key)set(key, value)

总结:

  • SetMap 允许你存储任何类型的键和值,并且提供了丰富的 API 来操作这些数据。
  • WeakSetWeakMap 的主要特性是它们对键的弱引用,这有助于防止内存泄漏,特别是在处理大量数据时。然而,由于这种弱引用特性,它们不提供 size 属性和遍历方法。

38.什么是的事件冒泡和事件捕获,如何阻止?

事件冒泡

定义:当一个元素上的事件被触发时,该事件会从最具体的元素(即事件源)开始,逐级向上传播,直到最顶层的元素(通常是文档对象)被触发。
阻止方法event.stopPropagation() 阻止事件冒泡到 DOM 树中的更高层元素。

element.addEventListener(
  "click",
  function (event) {
    event.stopPropagation();
    // 你的代码逻辑
  },
  false
);

addEventListener方法的第三个参数中传入false可以确保监听器在冒泡阶段处理事件,这是默认的行为。如果你想在捕获阶段处理事件,你需要将第三个参数设置为true

事件捕获
定义:事件捕获是从文档的最外层开始,逐级向下传播,直到达到事件源。在事件捕获过程中,首先会触发最外层元素的事件处理函数,然后依次触发内部元素的事件处理函数。
阻止方法:使用event.stopImmediatePropagation()方法阻止当前事件处理程序继续执行,并且也会阻止事件传播(包括捕获和冒泡)。

element.addEventListener(
  "click",
  function (event) {
    event.stopImmediatePropagation();
    // 你的代码逻辑
  },
  true
); // 注意这里设置为true,以便在捕获阶段处理事件

在这个例子中,通过将addEventListener的第三个参数设置为true,我们确保监听器在捕获阶段处理事件。然后,使用event.stopImmediatePropagation()来阻止事件继续传播。

39.JavaScript 中 preventDefault()方法有什么作用?

在 JavaScript 中,e.preventDefault()是一个事件处理函数,用于阻止事件的默认行为。

当一个事件触发时,浏览器会执行默认的操作。例如,当用户点击一个链接时,浏览器会加载新的页面;当用户提交一个表单时,浏览器会重新加载页面。通过调用 e.preventDefault(),可以取消或阻止这些默认行为的发生。

常见的使用场景包括:

  1. 点击链接时阻止页面跳转;
  2. 提交表单时阻止页面重新加载;
  3. 拖拽元素时阻止元素默认的拖拽行为;
  4. 阻止键盘按键的默认行为等。

总而言之,e.preventDefault()用于阻止事件的默认行为,以便开发者可以在事件触发时执行自定义的操作。

40.什么是闭包?闭包的用例有哪些?

闭包: 闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量;
原理: 当一个函数内部定义了另一个函数,并且内部函数引用了外部函数的变量时,就形成了闭包;
例子:

function outerFunction(outerVariable) {
  return function innerFunction(innerVariable) {
    console.log("outerVariable:", outerVariable);
    console.log("innerVariable:", innerVariable);
    console.log(
      "outerVariable + innerVariable:",
      outerVariable + innerVariable
    );
  };
}

const myClosure = outerFunction(5);
myClosure(3); // 输出: outerVariable: 5, innerVariable: 3, outerVariable + innerVariable: 8

特性:

  • 函数嵌套函数
  • 函数内部可以引用外包的参数和变量;
  • 参数和变量不会被垃圾回收机制回收;

优点:

  • 保护函数内变量的安全;
  • 方便调用访问上下文的局部变量;
  • 可以用来定义私有属性和私有方法;
  • 可以重复使用变量,并且不会造成变量污染;

缺点:

  • 常驻内存中,会增大内存使用量,使用不当很容易造成内存泄漏;
  • 会造成内存的浪费,这个内存浪费不仅因为它长期存在于内存中,更因为对闭包的使用不当会造成无效内存的产生;

作用:

  • 数据封装和私有变量:闭包可以用于创建私有变量,只能通过特定的公开方法进行访问和修改。这种方法可以隐藏内部实现细节,提供更为安全和稳定的接口。
  • 回调函数和高阶函数 :闭包常用于实现回调函数和高阶函数,因为它们可以记住其定义时的上下文。这使得闭包在处理异步操作、事件监听等场景时特别有用,因为它们可以确保在回调函数执行时能够访问到正确的变量和数据。
  • 函数防抖:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。实现的关键就在于 setTimeOut 这个函数,由于还需要一个变量来保存计时,考虑维护全局纯净,可以借助闭包来实现。
  • 迭代器:闭包可以用于创建迭代器,用于遍历数据集合。通过闭包,我们可以实现自定义的迭代逻辑,并在每次迭代时保留状态,确保迭代的正确性和一致性。
  • 柯里化(Currying):闭包可以用于实现函数的柯里化,将多个参数的函数转换成一系列使用一个参数的函数。这种方法可以简化函数调用,提高代码的可读性和可维护性。
  • 记忆化函数:闭包也可以用于实现记忆化函数,即函数将之前计算过的结果存储起来,在下次需要相同结果时直接返回,而不需要重新计算。这种方法可以显著提高计算密集型任务的性能。

41.浅拷贝和深拷贝区别概念常见情况?

浅拷贝

浅拷贝只复制对象的第一层属性。如果对象的属性值是一个对象或数组,那么实际上复制的是这个内部对象的引用,而不是真正的对象本身。因此,修改新对象中的这些引用类型的属性会影响到原对象。

例如,如果我们有一个包含对象的数组,当我们对这个数组进行浅拷贝时,新数组中的元素仍然是原数组中对象的引用,而不是新的对象。因此,如果我们修改了新数组中的对象,原数组中的对象也会被修改。

深拷贝

深拷贝会递归地复制对象及其所有的子对象。也就是说,它会创建一个新的对象,并复制原对象及其所有子对象的所有属性和值。这样,新对象和原对象是完全独立的,修改新对象不会影响到原对象。

在 JavaScript 中,实现深拷贝并不简单,因为需要处理各种复杂的数据类型和循环引用的情况。一种常见的实现深拷贝的方法是使用 JSON 的序列化和反序列化,即JSON.stringifyJSON.parse。但这种方法不能处理函数和循环引用的情况,因此并不完美。对于更复杂的情况,可能需要使用递归或其他更复杂的算法来实现深拷贝。

常见情况

  1. 基本数据类型:对于基本数据类型(如 Number、String、Boolean、Undefined、Null、Symbol),浅拷贝和深拷贝实际上是一样的,因为它们都是值类型,复制的是值本身。
  2. 引用数据类型:对于引用数据类型(如 Object、Array、Function),浅拷贝和深拷贝的区别就显现出来了。浅拷贝只复制引用,而深拷贝会复制引用指向的对象。
  3. 嵌套对象:当处理嵌套对象时,浅拷贝的问题尤为突出。因为浅拷贝只会复制最外层的引用,而内部的对象仍然是共享的。这可能导致在修改新对象时,原对象也被意外修改。而深拷贝则可以避免这个问题,因为它会递归地复制所有的子对象。

42.解释 Javascript 中的展开运算符是什么?

在 JavaScript 中,展开运算符(Spread Operator)是一种语法,它允许我们将一个可迭代对象(如数组或对象)的元素或属性展开到新的数组或对象中,或者在函数调用时作为独立的参数传递。

作用
展开运算符的主要作用是简化数组和对象的操作,包括:

  1. 复制数组或对象:可以轻松地创建数组或对象的浅拷贝。
  2. 合并数组或对象:将多个数组或对象的元素/属性合并到一个新的数组或对象中。
  3. 函数参数传递:将数组的元素或对象的属性作为独立的参数传递给函数。

优点

  1. 代码简洁:展开运算符允许我们以更简洁的方式执行常见的数组和对象操作,减少了冗余代码。
  2. 灵活性:它可以用于多种情况,包括数组和对象的合并、复制和函数参数传递。
  3. 易读性:对于熟悉 JavaScript 的开发者来说,展开运算符的语义相对直观,易于理解。

缺点

  1. 浅拷贝:展开运算符执行的是浅拷贝,如果数组或对象包含嵌套的对象或数组,那么这些嵌套的对象或数组不会被完全复制,而是共享引用。这可能导致意外的副作用,例如修改原始数组或对象中的嵌套结构时,也会影响到使用展开运算符创建的副本。
  2. 性能开销:虽然展开运算符使代码更简洁,但在处理大型数组或对象时,它可能会比传统的循环或迭代方法产生更大的性能开销。

应用场景

  1. 数组操作

    • 合并数组:const arr3 = [...arr1, ...arr2];
    • 复制数组:const arrCopy = [...arr];
    • 向数组添加元素:arr.push(...otherArray);
    • 在函数调用时传递数组元素:func(...array);
  2. 对象操作

    • 合并对象:const obj2 = { ...obj1, prop: 'value' };
    • 复制对象:const objCopy = { ...obj };

43.事件扩展符用过吗(…),说说原理,优缺点和使用场景?

事件扩展符(Spread operator)是 ES6 中引入的一种新语法,用于将一个数组或对象的元素/属性展开

原理

扩展符的原理基于迭代协议。当一个对象实现了[Symbol.iterator]()方法时,它就被认为是一个可迭代对象。扩展符内部使用这个方法来获取对象的迭代器,并迭代展开对象的元素。对于数组,扩展符将数组中的每个元素作为独立的参数或项展开;对于对象,它复制对象的所有可枚举属性到新对象中。

优点

  1. 简洁性:扩展符提供了一种简洁的方式来展开数组或对象的元素。
  2. 代码可读性:使用扩展符可以使代码更加清晰,减少冗余。
  3. 灵活性:扩展符可以方便地与其他 ES6 特性(如模板字符串、解构赋值等)结合使用,实现复杂的操作。

缺点

  1. 浏览器兼容性:扩展符是 ES6 引入的特性,因此在一些旧版本的浏览器中可能不受支持。
  2. 误用风险:如果不正确地使用扩展符(例如,在不适合的上下文中使用),可能会导致意外的行为或错误。

使用场景

  1. 数组展开:在函数调用时,将数组的元素作为单独的参数传入。
function sum(a, b, c) {
  return a + b + c;
}
const numbers = [1, 2, 3];
console.log(sum(...numbers)); // 输出:6
  1. 对象展开:在构造新的对象时,将一个对象的所有可枚举属性复制到新对象中。
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 };
console.log(obj2); // 输出:{ a: 1, b: 2, c: 3 }
  1. 事件监听器:尽管不常见,但理论上,你可以使用扩展符将多个处理函数组合成一个数组,并通过某种方式将它们绑定到事件监听器上。不过,这通常需要额外的逻辑,并且不是扩展符的典型用法。

44.说一说 es6 中箭头函数?

箭头函数是 ES6(ECMAScript 2015)引入的一种新的函数表达式形式。它使用 => 符号来定义函数,提供了一种更简洁、更直观的函数书写方式。

优点

  1. 简洁性:箭头函数的语法比传统函数更简洁,特别是当函数体较短时。
  2. this 的绑定:解决了传统函数中this指向不确定的问题,使得在回调函数中能够更清晰地访问外层作用域的this

缺点

  1. 不能用作构造函数:由于箭头函数没有自己的thisprototype属性,因此它不能被用作构造函数。尝试使用new操作符与箭头函数一起会抛出错误。
  2. 没有arguments对象:在箭头函数中,你不能使用arguments对象来访问函数参数。如果需要访问所有参数,可以使用剩余参数(rest parameters)。
  3. 不适用于所有场景:虽然箭头函数在许多情况下都非常有用,但并不是所有场景都适用。例如,当需要动态地绑定this或者需要函数具有自己的arguments对象时,传统函数可能更为合适。

总的来说,箭头函数提供了一种更简洁、更直观的方式来编写函数,特别是在处理this和回调函数的场景中。然而,它也有一些限制和不适用的场景,因此在使用时需要根据具体需求进行选择。

45.简述箭头函数和普通函数的区别?箭头函数能当构造函数吗?

箭头函数与普通函数之间存在几个显著的区别:

  1. 外形与语法:箭头函数使用箭头 => 定义,这使得其语法更加简洁。相比之下,普通函数使用 function 关键字定义。
  2. this 绑定:在箭头函数中,this 的值被永久地绑定到定义它的上下文中,不会因为函数的调用方式而改变。这使得在回调函数中处理 this 变得简单。而普通函数的 this 指向则取决于函数的调用方式,它可能指向全局对象、调用它的对象,或者在某些情况下是 undefined
  3. 作为构造函数:箭头函数不能用作构造函数,因为它们没有自己的 this 绑定和 prototype 属性。而普通函数则可以用作构造函数,通过 new 关键字来创建对象实例。
  4. arguments 对象:每一个普通函数调用后都具有一个 arguments 对象,用来存储实际传递的参数。但箭头函数并没有此对象,如果需要类似的功能,可以使用剩余参数(rest parameters)。
  5. 其他特性:箭头函数不具有 prototype 原型对象、supernew.target。这些特性在普通函数中都是存在的。

46.请举出一个匿名函数的典型案例?

在 JavaScript 中,匿名函数是一种没有名称的函数,它经常被用作回调函数,事件处理程序,或者创建闭包。以下是一个典型的匿名函数使用的案例:
这是一个使用匿名函数作为数组排序方法的回调函数的例子:

var numbers = [40, 1, 5, 200];

numbers.sort(function (a, b) {
  return a - b;
});

console.log(numbers); // 输出: [1, 5, 40, 200]

在这个例子中,我们使用了数组的 sort() 方法来对 numbers 数组进行排序。sort() 方法接受一个可选的比较函数作为参数,该函数用于确定数组元素的排序顺序。在这个例子中,我们传递了一个匿名函数作为 sort() 方法的参数,这个匿名函数接收两个参数 a 和 b,然后返回它们的差,这样就能决定数组元素的排序顺序。

47.说一说 this 指向(普通函数、箭头函数)?

在 JavaScript 中,this 的指向是一个复杂但非常关键的概念。它决定了函数内部引用的是哪个对象。对于普通函数和箭头函数,this 的指向规则是不同的。

普通函数中的this
普通函数中的 this 指向是在函数被调用时确定的,而不是在函数定义时。具体来说,this 的指向取决于调用函数的方式。

  1. 全局环境:在全局环境(非严格模式)中调用函数,this 通常指向全局对象(在浏览器中是 window)。
function regularFunction() {
  console.log(this); // 在浏览器中通常输出 window
}
regularFunction();
  1. 作为对象方法:当函数作为对象的方法被调用时,this 指向该对象。
const obj = {
  property: "Hello",
  method: function () {
    console.log(this.property); // 输出 'Hello'
  },
};
obj.method();
  1. 构造函数:当函数用作构造函数(通过 new 关键字调用)时,this 指向新创建的对象实例。
function Constructor() {
  this.value = "Constructor called";
}
const instance = new Constructor();
console.log(instance.value); // 输出 'Constructor called'
  1. 通过 callapplybind 方法 :这些方法允许你显式地设置函数调用的 this 值。
function exampleFunction() {
  console.log(this.value);
}
const obj = { value: "Called with obj" };
exampleFunction.call(obj); // 输出 'Called with obj'

箭头函数中的 this
箭头函数在处理 this 时有一个重要的特点:它们不绑定自己的 this,而是捕获其所在上下文的 this 值作为自己的 this 值。这意味着箭头函数中的 this 实际上是在定义时确定的,而不是在调用时。

const obj = {
  value: "Hello from obj",
  arrowMethod: () => {
    console.log(this.value); // 这里的 this 不是指向 obj,而是定义箭头函数时的上下文(可能是全局对象或undefined,取决于严格模式)
  },
};
obj.arrowMethod(); // 输出可能是全局对象上的 value 属性,或者在严格模式下是 undefined

由于箭头函数不绑定自己的 this,所以它们经常用于需要保持 this 上下文不变的场景,比如在回调函数或事件处理程序中。

了解 this 在不同情况下的指向是 JavaScript 编程中的一个关键概念,对于编写健壮和可维护的代码至关重要。

48.解释 JavaScript 中的 this 关键字的作用和使用场景?

this的作用:

  1. 引用当前对象this 允许你引用当前对象(或上下文)的属性或方法。
  2. 实现面向对象编程:在构造函数或对象方法中,this 通常用于引用新创建的对象实例。
  3. 动态上下文this 的值在运行时确定,取决于函数如何被调用。

this 的使用场景:

  1. 全局上下文:在全局作用域中,this 通常指向全局对象(在浏览器中是 window 对象)。
console.log(this === window); // true
  1. 函数调用:在普通函数调用中,this 通常指向全局对象(除非在严格模式下,此时 thisundefined)。
function myFunction() {
  console.log(this);
}
myFunction(); // 指向全局对象(window)
  1. 对象方法:当函数作为对象的方法被调用时,this 指向该对象。
const obj = {
  prop: "Hello",
  method: function () {
    console.log(this.prop); // 输出 'Hello'
  },
};
obj.method();
  1. 构造函数:在构造函数中,this 指向新创建的对象实例。
function MyConstructor() {
  this.prop = "Hello";
}
const instance = new MyConstructor();
console.log(instance.prop); // 输出 'Hello'
  1. 事件处理器:在 DOM 事件处理器中,this 通常指向触发事件的元素。
const button = document.getElementById("myButton");
button.addEventListener("click", function () {
  console.log(this); // 指向按钮元素
});
  1. 箭头函数:箭头函数不绑定自己的 this,它会捕获其所在上下文的 this 值,作为自己的 this 值。
const obj = {
  prop: "Hello",
  arrowMethod: () => {
    console.log(this); // 指向全局对象(window),而不是 obj
  },
};
obj.arrowMethod();

49.简述 JavaScript 构造函数的特点?

特点:

  1. 函数名与类名相同:构造函数的名称通常与创建的类的名称相同。
  2. 无返回值:构造函数不需要定义返回值类型,也不需要写 return 语句。
  3. 可以重载:构造函数可以重载,即可以定义多个同名但参数列表不同的构造函数。
  4. 初始化对象:构造函数的主要功能是初始化对象,而不是创建对象。
  5. 自动调用:构造函数会在创建新对象时自动调用。
  6. 默认构造函数:如果用户没有显式地定义构造函数,JavaScript 系统会自动调用默认构造函数。但是,一旦用户显式地定义了构造函数,系统就不再调用默认构造函数。

优点:

  1. 对象初始化:构造函数用于初始化新创建的对象,确保每个对象在创建时都具有正确的初始状态。
  2. 封装性:构造函数可以封装与类相关的属性和方法,提高代码的可读性和可维护性。
  3. 继承性:通过构造函数,可以实现对象之间的继承关系,使子类对象可以继承父类对象的属性和方法。

缺点:

  1. 可能导致性能问题:如果构造函数过于复杂,或者频繁创建对象,可能会影响性能。
  2. 过度使用可能导致代码冗余:如果每个对象都需要通过构造函数进行初始化,而初始化过程又非常相似,那么可能会导致代码冗余。

应用场景:

  1. 创建具有相同属性和方法的多个对象:当需要创建多个具有相同属性和方法的对象时,可以使用构造函数来定义这些对象的共同特征。
  2. 实现继承:当需要实现对象之间的继承关系时,可以使用构造函数来定义父类和子类,子类对象可以继承父类对象的属性和方法。
  3. 封装复杂逻辑:当需要将复杂的初始化逻辑封装在一个函数中时,可以使用构造函数来实现。

与普通函数的区别:

  1. 目的不同:普通函数主要用于执行特定的任务或计算,而构造函数则主要用于初始化新创建的对象。
  2. 调用方式不同:普通函数可以直接调用,而构造函数需要通过 new 关键字来创建对象。
  3. 返回值不同:普通函数可以返回任意类型的值,而构造函数则返回一个新创建的对象。

50.如果一个构造函数,bind 了一个对象,用这个构造函数创建出的实例会继承这个对象的属性吗?为什么?

不会,因为当一个构造函数被 bind 到一个对象时,实际上你创建了一个新的函数,这个新函数在被调用时,其 this 会被设置为 bind 的那个对象。然而,这并不意味着用这个构造函数创建出的实例会继承那个对象的属性。

原因如下:

  1. bind的作用:bind返回一个新的函数,这个新的函数在被调用时,其this值会被设置为提供的值(也就是被bind` 的对象)。这并不会改变原构造函数的原型链或任何其它特性。

  2. 实例的创建:当你使用构造函数来创建实例时(比如通过 new 关键字),JavaScript 会按照以下步骤操作:

    • 创建一个新的空对象。
    • 将这个新对象的 __proto__ 属性设置为构造函数的 prototype 对象。
    • 将构造函数中的 this 指向这个新对象。
    • 执行构造函数中的代码。
    • 如果构造函数没有返回其它对象,则返回这个新对象。

由于 bind 只是改变了 this 的指向,并没有改变构造函数的原型链,因此实例不会从被 bind 的对象中继承属性。实例只会从构造函数的原型中继承属性。

举个例子:

function MyConstructor() {
  this.myProperty = "Hello";
}

const myObject = { objProperty: "World" };

const boundConstructor = MyConstructor.bind(myObject);

const instance = new boundConstructor();

console.log(instance.myProperty); // 输出 'Hello'
console.log(instance.objProperty); // 输出 undefined,因为 objProperty 不是从 myObject 继承的

在这个例子中,尽管 MyConstructorbind 到了 myObject,但是使用 boundConstructor 创建的 instance 仍然只继承了 MyConstructor.prototype 上的属性(如果有的话),而不是 myObject 的属性。instancethis 在构造函数执行期间确实指向了 myObject,但这仅仅影响了构造函数内部的代码执行上下文,并不影响实例的原型链。

51.简述 JavaScript 中的高阶函数是什么?

高阶函数(Higher-order function)在 JavaScript 中指的是那些可以接收其他函数作为参数,或者返回一个函数的函数。这是函数式编程的一个核心概念,使得函数可以作为其他函数的输入或输出,从而极大地提高了代码的灵活性和复用性。

以下是一些具体的例子:

  • map:这个方法会对数组中的每个元素执行一个函数,并返回一个新的数组,新数组中的元素是原数组元素执行函数后的结果。
const numbers = [1, 2, 3];
const doubled = numbers.map(function (n) {
  return n * 2;
});
console.log(doubled); // 输出 [2, 4, 6]
  • filter:这个方法会创建一个新数组,新数组中的元素是通过检查指定函数而得出的所有元素。
const numbers = [1, 2, 3, 4, 5, 6];
const evenNumbers = numbers.filter(function (n) {
  return n % 2 === 0;
});
console.log(evenNumbers); // 输出 [2, 4, 6]
  • reduce:这个方法对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个输出值。
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce(function (accumulator, currentValue) {
  return accumulator + currentValue;
}, 0);
console.log(sum); // 输出 15

除了数组方法,JavaScript 中还有许多其他的高阶函数,例如 setTimeoutPromisethen 方法等。高阶函数的使用使得代码更加模块化,提高了代码的可读性和可维护性。

52.解释 Javascript 中的回调函数 ?为什么使用回调函数?

回调函数的基本概念

回调函数本质上就是一个函数,但它不是立即执行的,而是在特定的条件下或特定的时间点由另一个函数来调用。这种模式的主要特点是:一个函数(我们称之为主函数)接受另一个函数(我们称之为回调函数)作为参数,并在其执行过程中根据需要调用这个回调函数。

为什么使用回调函数
使用回调函数的主要原因有以下几点:

  1. 异步处理:在 JavaScript 中,很多操作,如网络请求、文件读写或定时任务,都是异步的。这意味着这些操作不会立即完成,而是需要一些时间。使用回调函数,我们可以在这些操作完成时执行特定的代码,而不是阻塞主线程等待它们完成。

  2. 事件监听:在事件驱动编程中,我们经常需要监听特定的事件(如按钮点击、鼠标移动等),并在这些事件发生时执行相应的代码。这些事件处理程序通常作为回调函数传递给事件监听器。

  3. 分步执行:有时,我们需要按照特定的顺序执行一系列操作,并且每个操作都依赖于前一个操作的结果。使用回调函数,我们可以将每个操作封装在一个函数中,并将这些函数作为参数传递给其他函数,以实现分步执行。

  4. 代码复用:通过将回调函数作为参数传递,我们可以实现代码的复用。不同的函数可以接受相同的回调函数,并在需要的时候调用它,从而避免了重复编写相同的代码。

  5. 代码模块化:回调函数有助于将代码拆分成更小的、更易于管理的模块。每个模块可以专注于一个特定的任务,并通过回调函数与其他模块进行通信。

示例
下面是一个简单的示例,演示了如何使用回调函数处理异步操作:

function fetchData(url, callback) {
  // 假设 fetchData 是一个模拟的网络请求函数
  setTimeout(() => {
    const data = "这是从服务器获取的数据";
    callback(data); // 在数据获取后调用回调函数
  }, 1000);
}
// 使用 fetchData 函数,并传递一个回调函数作为参数
fetchData("https://example.com/data", (data) => {
  console.log(data); // 输出:这是从服务器获取的数据
});

在这个示例中,fetchData 函数模拟了一个异步的网络请求。它接受一个 URL 和一个回调函数作为参数。当数据获取成功后,它调用回调函数并传递获取到的数据。这样,我们就可以在回调函数中处理这些数据,而不需要等待网络请求完成。

53.简述 Javascript isNan()函数?

isNaN() 是 JavaScript 中的一个全局函数,用于确定一个值是否是 “NaN”(Not a Number,即非数字)。这个函数接受一个参数,并返回一个布尔值:如果参数是 NaN,或者可以被转换为 NaN 的值(比如一个非数字字符串),那么返回 true;否则返回 false。

但是,需要注意的是,isNaN() 的行为并不总是完全符合直觉。例如,它会对空字符串、空对象、undefined 和 null 返回 false,尽管这些值在某种意义上也不是数字。

这是因为 isNaN() 在处理这些值时,会首先尝试将参数转换为数字。如果转换成功(例如,空字符串转换为 0,null 转换为 0,undefined 转换为 NaN),那么 isNaN() 将返回 false。只有当转换失败时(例如,一个包含字母的字符串),isNaN() 才会返回 true。

因此,对于非数字值的检查,更推荐使用 Number.isNaN() 函数。这个函数的行为更符合直觉:它只对真正的 NaN 和可以被转换为 NaN 的值(比如 “NaN” 字符串)返回 true,对其他所有非数字值返回 false。

例如:

console.log(isNaN("123")); // false,因为 "123" 可以被转换为数字
console.log(isNaN("abc")); // true,因为 "abc" 不能被转换为数字
console.log(isNaN(null)); // false,因为 null 可以被转换为 0
console.log(isNaN(undefined)); // false,因为 undefined 可以被转换为 NaN
console.log(Number.isNaN("123")); // false,因为 "123" 可以被转换为数字
console.log(Number.isNaN("abc")); // false,因为 "abc" 不能被转换为数字
console.log(Number.isNaN(null)); // false,因为 null 可以被转换为 0
console.log(Number.isNaN(undefined)); // true,因为 undefined 可以被转换为 NaN

总的来说,isNaN() 和 Number.isNaN() 都可以用来检查一个值是否是 NaN,但 Number.isNaN() 的行为更符合直觉,因此在可能的情况下,推荐使用 Number.isNaN()。

54.JavaScript 里函数参数 arguments 是数组吗?

在 JavaScript 中,arguments 对象不是一个真正的数组,而是一个类数组对象(array-like object)。尽管 arguments 对象可以使用数组索引(0, 1, 2, …)来访问元素,并且具有 length 属性,但它并不具备数组的全部方法和属性。

例如,你不能直接在 arguments 对象上使用 pushpopsliceforEach 等数组方法。如果你需要将这些方法应用于 arguments 对象,你可以通过将其转换为真正的数组来实现。这通常可以通过使用 Array.prototype.slice.call(arguments) 或更现代的 Array.from(arguments) 来完成。

以下是一个例子:

function exampleFunction() {
  var args = Array.from(arguments); // 或者使用 var args = Array.prototype.slice.call(arguments);
  args.forEach(function (arg) {
    console.log(arg);
  });
}
exampleFunction(1, 2, 3); // 输出 1, 2, 3

在这个例子中,arguments 对象被转换为一个真正的数组,然后我们可以在这个数组上使用 forEach 方法。

55.什么是柯里化函数?优缺点?

在 JavaScript 中,柯里化(Currying)是一种将使用多个参数的函数转换成一系列使用一个参数的函数的技术。每个这样的函数都返回下一个函数,直到最后一个函数返回计算结果。

柯里化函数的优点:

  1. 参数复用:由于柯里化函数每次只接受一个参数,因此可以很容易地固定某些参数的值,从而创建新的函数。
  2. 延迟执行:柯里化允许你分步提供函数所需的参数,只有在所有参数都提供后才执行函数,这有助于实现延迟计算。
  3. 函数组合:柯里化函数可以很容易地与其他柯里化函数组合,以创建更复杂的函数。
  4. 代码可读性:每个柯里化函数都只处理一个参数,这使得代码更易于理解和维护。

柯里化函数的缺点:

  1. 性能开销:每次调用柯里化函数都会返回一个新的函数,这可能会导致额外的内存开销和性能损耗,特别是在大量使用柯里化函数的情况下。
  2. 可读性:对于不熟悉柯里化的开发者来说,代码可能会显得有些不直观,需要一定的学习成本。

如何实现柯里化:

在 JavaScript 中,实现柯里化函数的一个基本方法是通过递归和闭包。以下是一个简单的实现示例:

function curry(fn) {
  if (typeof fn !== "function") {
    throw new Error("curry() requires a function");
  }
  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));
      };
    }
  };
}

// 使用示例
function add(a, b, c) {
  return a + b + c;
}
const curriedAdd = curry(add);
const addTwo = curriedAdd(1); // 返回一个函数,等待剩余参数
const result = addTwo(2, 3); // 调用返回的函数,得到结果 6
console.log(result); // 输出 6

在上面的例子中,curry函数接受一个函数fn作为参数,并返回一个新的函数curriedcurried函数会根据传入的参数数量来决定是直接调用原始函数fn,还是返回一个新的函数来继续接收剩余的参数。通过递归和闭包,我们可以保证每次调用都返回一个新的函数,直到所有参数都提供完毕并执行原始函数。

需要注意的是,实现柯里化时,应该检查传入的参数数量与原始函数期望的参数数量,以确保在正确的时机执行原始函数。此外,现代的 JavaScript 库(如 lodash)已经提供了柯里化函数的实现,可以直接使用而无需手动实现。

56.简述你如何给一个事件处理函数命名空间,为什么要这样做?

在 JavaScript 中,给事件处理函数添加命名空间是一个有用的做法,它可以帮助我们更好地管理和控制事件的绑定和解除。以下是如何给一个事件处理函数添加命名空间的步骤,以及为什么要这样做的原因:

如何给事件处理函数添加命名空间

在绑定事件时,我们可以在事件名称后面添加一个自定义的字符串作为命名空间。这个字符串通常是一个点(.)后跟一个或多个标识符。例如:

element.addEventListener("click.myNamespace", function (event) {
  // 处理点击事件
});

在这个例子中,click.myNamespace就是一个带有命名空间的事件名称。

为什么要给事件处理函数添加命名空间

  1. 避免事件冲突:当我们在同一个元素上绑定多个相同类型的事件处理函数时,这些函数可能会相互冲突或覆盖。通过给每个事件处理函数添加唯一的命名空间,我们可以确保它们不会相互干扰。
  2. 方便解除事件绑定:当我们需要解除某个特定的事件处理函数时,如果我们没有给它添加命名空间,我们可能需要遍历所有绑定到该元素的事件处理函数,然后逐个比较它们来确定要解除哪一个。而如果我们使用了命名空间,我们就可以直接通过命名空间来快速定位并解除对应的事件处理函数。
  3. 代码组织和可读性:通过给事件处理函数添加命名空间,我们可以更好地组织代码,使其更具可读性。命名空间可以帮助我们区分不同的事件处理函数组,从而更容易地理解和管理它们。

总的来说,给事件处理函数添加命名空间是一种提高代码质量和可维护性的有效方法。它可以帮助我们避免事件冲突,方便解除事件绑定,并提高代码的组织性和可读性。

57.阐述 AMD 和 Commonjs 的理解?

理解:
AMD是异步模块定义,它是 RequireJS 在推广过程中对模块定义的规范化产出,规范加载模块是异步的,依赖必须提前声明好;
CommonJS是一种后端 js 规范,是 Node.js 遵循的一种编写 js 模块的规范,它定义了模块的基本格式,以及模块加载的方式,规范加载模块是同步的,只有加载完成,才能执行后面的操作。

区别:

  1. 应用环境
    AMD是为浏览器端设计的模块加载方式,特别适用于处理网络环境中的复杂性和不确定性。
    CommonJS则主要适用于服务器端,尤其是Node.js环境,它采用同步加载方式,非常适合处理存储在服务器硬盘上的模块文件。
  2. 加载方式
    AMD采用异步加载模块,这种方式可以非阻塞地加载和执行模块,提升了浏览器的响应性能,特别是在网络环境较差时更为优越。
    CommonJS则是同步加载模块,必须等待模块加载完成后才能执行后续代码。
  3. 依赖处理
    AMD推崇依赖前置,即模块定义时必须明确所有依赖,这种方式可以提前加载依赖,但可能造成不必要的资源浪费。
    CommonJS则按需加载依赖,即在运行时动态解析和处理依赖,这种方式更为灵活,但也可能导致运行时依赖未解决的情况。

58.JS 中的类是什么?

在 JavaScript 中,类(Class)是一种用户定义的类型,它提供了一种模板来创建具有相同属性和方法的对象。类是面向对象编程(OOP)的核心概念之一,它使得代码更加结构化、可维护和可复用。

类的好处主要体现在以下几个方面:

  1. 代码复用:通过定义类,我们可以创建多个具有相同属性和方法的对象,而无需重复编写相同的代码。这大大减少了代码的冗余,提高了代码的效率。
  2. 封装性:类可以将对象的属性和方法封装在一起,形成一个独立的单元。这有助于隐藏对象的内部状态和实现细节,只暴露必要的接口给外部使用。封装性增强了代码的安全性和可维护性。
  3. 继承性:JavaScript 中的类支持继承机制,子类可以继承父类的属性和方法。这使得我们可以创建具有层次结构的类,实现代码的复用和扩展。通过继承,子类可以继承父类的功能,并添加或覆盖自己的功能,从而构建更复杂、更灵活的对象。
  4. 多态性:多态性是面向对象编程的另一个重要特性,它允许使用父类类型的引用来引用子类对象。在 JavaScript 中,通过类的继承和方法的重写,我们可以实现多态性,使得不同类的对象可以响应相同的消息或方法调用,并执行各自特定的操作。

此外,使用类还有助于提高代码的可读性和可维护性。通过将相关的属性和方法组织在一个类中,我们可以更清晰地表达对象的结构和行为,使得代码更易于理解和修改。

总的来说,JavaScript 中的类提供了一种强大而灵活的方式来创建和管理对象,它使得面向对象编程在 JavaScript 中变得更加直观和高效。通过使用类,我们可以构建出结构清晰、可维护、可复用的代码,提高软件开发的效率和质量。

59.解释 JavaScript 中的异步编程,并提供一个异步操作的示例。

JavaScript 中的异步编程是指代码在执行时不会按照顺序立即完成,而是会等待某些操作(如网络请求、文件读写、定时器)完成后再继续执行。这种编程方式使得 JavaScript 能够处理耗时的操作,而不会阻塞主线程,从而提高应用程序的性能和响应能力。

优点:

  1. 非阻塞:异步操作不会阻塞主线程,允许其他代码继续执行。
  2. 高响应性:用户界面不会因为长时间运行的任务而变得无响应。
  3. 资源利用:在等待异步操作完成期间,主线程可以处理其他任务,从而更有效地利用系统资源。

缺点:

  1. 编程复杂度:异步编程可能使代码更难理解和维护,尤其是当涉及到多个异步操作时。
  2. 错误处理:在异步代码中,错误处理可能变得复杂,需要额外的注意和技巧。
  3. 回调地狱(Callback Hell):使用回调函数进行异步编程时,可能会导致嵌套层级过深,使得代码难以阅读和理解。

应用场景:

  1. 网络请求:如 AJAX、Fetch API 用于从服务器获取数据。
  2. 文件读写:Node.js 中的文件系统操作。
  3. 定时器:setTimeout、setInterval 用于延迟执行或周期性执行代码。
  4. Web Workers:在浏览器端执行复杂的计算任务,不阻塞主线程。
// 创建一个返回 Promise 的函数,模拟异步操作
function fetchData() {
  return new Promise((resolve, reject) => {
    // 假设这里是一个异步操作,例如网络请求
    setTimeout(() => {
      const data = "获取到的数据";
      resolve(data); // 异步操作成功时调用 resolve
    }, 1000);
  });
}

// 使用 async/await 调用 fetchData 函数
async function handleData() {
  try {
    const data = await fetchData(); // 等待 fetchData 函数的异步操作完成
    console.log(data); // 输出:获取到的数据
  } catch (error) {
    console.error("获取数据失败", error);
  }
}

handleData(); // 调用 handleData 函数开始异步操作

60.解释 JavaScript 中的模块化编程,并提供一个使用模块的示例?

在 JavaScript 中,模块化编程是一种将代码拆分成多个独立、可重用的模块的方法。每个模块都封装了特定的功能或数据,并且只暴露必要的接口供其他模块使用。模块化编程有助于组织代码,提高代码的可读性和可维护性,并促进代码的重用和协作。

ES6 模块化支持两种类型的模块:

  1. CommonJS 模块:这是 Node.js 使用的模块系统,通过 require 导入模块,通过 module.exports 导出模块。
  2. ES6 模块:这是 ECMAScript 2015 引入的原生模块系统,通过 import 导入模块,通过 export 导出模块。

下面是一个使用 ES6 模块的示例:

假设我们有一个名为 mathFunctions.js 的模块,它包含一些数学函数:

// mathFunctions.js
export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}

然后,我们可以在另一个 JavaScript 文件中导入并使用这些函数:

// main.js
import { add, multiply } from "./mathFunctions.js";

console.log(add(2, 3)); // 输出 5
console.log(multiply(2, 3)); // 输出 6

61.解释 JavaScript 中的严格模式?

严格模式是 ES5 引入的一种 JavaScript 运行模式,它为 JavaScript 提供了一种更加严格的运行环境。这种模式通过在代码开头添加一个特定的编译指示符“use strict”来启用。严格模式的设计目的是使 JavaScript 代码在更严格的条件下执行,以便开发者更容易发现并修正错误,使代码更加健壮、安全和高效。

在严格模式下,JavaScript 引擎会执行更严格的语法和运行时检查,包括但不限于以下几点:

  1. 变量必须先声明后使用。未声明的变量在严格模式下会被视为错误。
  2. 严格模式不允许删除变量、函数或函数的参数。尝试这样做会导致运行时错误。
  3. 在严格模式下,this 关键字的行为也发生了一些变化。例如,全局上下文中的 this 不再指向全局对象(如 window),而是 undefined,除非函数被显式地通过 call()apply() 方法调用。

优点:

  1. 帮助捕获常见错误:通过引入更严格的语法和运行时检查,严格模式能够帮助开发者捕获到一些在宽松模式下可能会被忽略的错误。比如,变量必须先声明后使用,未声明的变量会导致错误;删除变量、函数或函数参数在严格模式下也是不允许的。

  2. 提高代码质量和可维护性:由于严格模式对代码进行了更严格的检查,这有助于开发者编写更加规范、高质量的代码。同时,严格的错误检查也使得代码调试更加容易,能够更早地发现并修复潜在的问题,从而提高代码的可维护性。

  3. 防止全局污染:在严格模式下,全局变量必须显式声明,这有助于防止因意外创建全局变量而导致的全局污染问题。

缺点:

  1. 兼容性问题:虽然现代浏览器大多支持严格模式,但在一些较旧的浏览器或环境中可能存在兼容性问题。因此,在使用严格模式时,需要考虑到目标用户群体的浏览器兼容性情况。

  2. 学习成本:对于不熟悉严格模式的开发者来说,可能需要一定的学习成本来适应这种更严格的编程环境。需要了解哪些操作在严格模式下是被禁止的,以及如何正确地编写符合严格模式要求的代码。CommonJS推崇依赖就绪,只有在代码执行时,才去 require 所依赖的模块。

62.Javascript 中, 什么是 use strict?使用它的好处和坏处分别是什么?

在 JavaScript 中,“use strict” 是一种特殊的字面量表达式,它被用在脚本或函数的开头,用于启用严格模式。严格模式使得 JavaScript 在执行时更加严格,有助于捕捉一些常见的编码错误,如使用未声明的变量等。

使用 “use strict” 的好处:

  1. 防止全局变量:在严格模式下,如果你尝试隐式地创建一个全局变量(例如,忘记使用 var 关键字),JavaScript 会抛出一个错误。这有助于防止意外的全局污染。
  2. 更严格的错误检查:严格模式会执行更严格的错误检查,例如,当你尝试删除一个不可删除的属性时,它会抛出一个错误。
  3. 防止函数参数重名:在严格模式下,函数中的参数不能有相同的名称。
  4. 防止对象字面量属性的重复:在严格模式下,对象字面量不能有重复的属性名称。
  5. 更好的性能:严格模式可以帮助 JavaScript 引擎进行某些优化,从而提高代码的性能。
  6. 改进调试:严格模式可以使调试过程更容易,因为它有助于识别潜在的错误。

使用 “use strict” 的坏处:

  1. 代码兼容性:不是所有的 JavaScript 代码都能在严格模式下运行。一些旧的库和框架可能无法与严格模式兼容。
  2. 增加代码复杂度:如果你的项目中的代码不需要严格模式的特性,那么在代码中添加"use strict"可能会增加不必要的复杂性。
  3. 额外的错误检查:虽然严格模式可以帮助识别错误,但它也可能导致一些在宽松模式下可以运行的代码在严格模式下失败。这可能需要额外的调试和修复工作。

总的来说,"use strict"是一个有用的工具,可以帮助你编写更安全、更可靠的 JavaScript 代码。然而,在使用它之前,你应该确保你的代码和依赖项都能够在严格模式下运行,并且你准备好处理可能出现的额外错误检查。

63.解释什么是工厂模式,有什么优缺点?

工厂模式 属于设计模式的创建型模式,通过实现共同的抽象接口创建属于同一类类型的不同对象实现,隐藏了对象创建的逻辑,提供了一种创建对象的最佳方式。
优点:屏蔽产品对象的具体实现,使调用者只关注接口;扩展性高,如果需要增加产品,只需要添加工厂类就可以,无需修改源代码;通过名字就可以创建想要的对象。
缺点:每增加一个产品类就要增加一个具体的产品类和工厂类,系统中的类成倍增加,增加了类的复杂度。

64.JavaScript 原型、原型链?有什么特点?

在 JavaScript 中,原型(prototype)和原型链(prototype chain)是面向对象编程的两个核心概念,它们对于理解 JavaScript 中的继承机制至关重要。

原型(Prototype)

在 JavaScript 中,每个函数都有一个prototype属性,这个属性是一个指向对象的引用。这个对象包含了可以由特定类型的所有实例共享的属性和方法。换句话说,prototype允许我们为对象的类型定义方法和属性。当创建一个新的对象实例时,这个实例会内部链接到这个prototype对象,从而可以访问其上的属性和方法。

原型链(Prototype Chain)

原型链是 JavaScript 实现对象间继承关系的一种方式。当一个对象试图访问一个属性时,如果这个对象本身并没有这个属性,那么 JavaScript 引擎会查找这个对象的__proto__属性(也就是它的原型对象)以寻找这个属性。如果原型对象也没有这个属性,那么引擎会继续查找原型对象的__proto__属性(也就是它的原型对象的原型对象),如此类推,形成一条链式结构,直到找到所需的属性或到达链的末尾(通常是Object.prototype)。这就是所谓的原型链。

特点

  1. 继承性:通过原型链,对象可以继承其原型对象的属性和方法,从而实现代码的重用和扩展。
  2. 动态性:原型和原型链是动态的,可以在运行时修改。这意味着可以随时向原型对象添加新的属性和方法,这些新的属性和方法会立即对所有基于该原型的对象可用。
  3. 性能考虑:虽然原型链提供了灵活的继承机制,但频繁地沿着原型链查找属性可能会对性能产生影响。因此,在设计对象结构时需要考虑性能优化。
  4. 覆盖与优先级:如果对象本身和它的原型都定义了一个同名属性,那么优先读取对象本身的属性,这被称为“覆盖”。这意味着对象自身的属性会覆盖原型链上的同名属性。

65.解释 JavaScript 中的事件委托是什么,并提供一个使用事件委托的示例?

事件委托(Event Delegation)是 JavaScript 中的一个重要概念,它主要利用事件冒泡的原理,将事件监听器添加到父元素上,而不是直接添加到目标元素上。这样,当在父元素内部的某个子元素上触发事件时,这个事件会冒泡到父元素,从而触发父元素上的事件监听器。

使用事件委托的好处主要有以下几点:

  1. 减少内存占用:只需要给父元素添加事件监听器,而不需要给每个子元素都添加,从而减少了内存占用。
  2. 动态内容处理:对于动态添加到父元素中的子元素,即使它们没有直接绑定事件监听器,也可以处理它们的事件,因为事件监听器是绑定在父元素上的。

下面是一个使用事件委托的示例:

假设我们有一个包含多个按钮的列表,每个按钮都有一个 click 事件。我们想要使用事件委托来处理这些按钮的点击事件。

<!-- HTML 代码:-->
<div id="button-container">
  <button class="my-button">按钮 1</button>
  <button class="my-button">按钮 2</button>
  <button class="my-button">按钮 3</button>
  <!-- 这里可以动态添加更多的按钮 -->
</div>
// JavaScript 代码:
document
  .getElementById("button-container")
  .addEventListener("click", function (event) {
    // 检查被点击的元素是否是我们要处理的按钮
    if (event.target.matches("button.my-button")) {
      alert("你点击了按钮: " + event.target.textContent);
    }
  });

在这个示例中,我们给 button-container 这个父元素添加了一个 click 事件监听器。当用户点击任何一个 my-button 类的按钮时,由于事件冒泡,这个点击事件会冒泡到 button-container 元素,并触发我们添加的事件监听器。然后,我们通过 event.target 检查被点击的元素是否是我们要处理的按钮,如果是,就执行相应的操作。

66.解释 JavaScript 中的原型继承是什么?

JavaScript中的原型继承 就是对象继承自其原型对象,在对象的原型对象中添加属性,该对象就会自动继承得到。

JavaScript 中的原型继承是通过原型链实现的,每个对象都有一个指向其原型(prototype)的内部链接。当试图访问一个对象的属性时,如果该对象内部不存在这个属性,那么 JavaScript 会在对象的原型上寻找这个属性,这就是原型链。通过原型链,一个对象可以继承其原型对象的属性和方法。在 JavaScript 中,可以使用 prototype 属性来为一个对象添加属性和方法,这些属性和方法将被该对象的所有实例继承。

67.简述 JavaScript 中现实对象继承的几种方式?

  1. 原型链继承
function Parent() {
  this.name = "Parent";
}

Parent.prototype.getName = function () {
  return this.name;
};

function Child() {
  this.age = 25;
}

Child.prototype = new Parent(); // 设置原型链指向 Parent 的实例

var child = new Child();
console.log(child.getName()); // 输出 'Parent'
  1. 构造函数继承
function Parent(name) {
  this.name = name;
}

function Child(name, age) {
  Parent.call(this, name); // 使用call将Parent作为构造函数调用
  this.age = age;
}

var child = new Child("Parent", 25);
console.log(child.name); // 输出 'Parent'
  1. 组合继承(原型链加构造函数)
function Parent(name) {
  this.name = name;
}

Parent.prototype.getName = function () {
  return this.name;
};

function Child(name, age) {
  Parent.call(this, name); // 使用call设置属性
  this.age = age;
}

Child.prototype = new Parent(); // 设置原型链

var child = new Child("Parent", 25);
console.log(child.getName()); // 输出 'Parent'
  1. 原型式继承
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

var parent = {
  name: "Parent",
  getName: function () {
    return this.name;
  },
};

var child = object(parent);
console.log(child.getName()); // 输出 'Parent'
  1. 寄生式继承
function createAnother(original) {
  var clone = object(original);
  clone.sayHi = function () {
    return "Hi";
  };
  return clone;
}

var parent = {
  name: "Parent",
  getName: function () {
    return this.name;
  },
};

var child = createAnother(parent);
console.log(child.sayHi()); // 输出 'Hi'
  1. ES6 类继承
class Parent {
  constructor(name) {
    this.name = name;
  }

  getName() {
    return this.name;
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 调用父类构造函数
    this.age = age;
  }
}

const child = new Child("Parent", 25);
console.log(child.getName()); // 输出 'Parent'

68.写出一个实现寄生式继承的方法?

寄生式继承是基于原型链和借用构造函数技术的一种混合继承模式。它借用了原型链的方式来实现继承,同时解决了借用构造函数继承中方法复用的问题。

以下是一个简单的寄生式继承的实现:

function createObject(proto) {
  function F() {}
  F.prototype = proto;
  return new F();
}

function inheritPrototype(subType, superType) {
  var prototype = createObject(superType.prototype); // 创建对象,以父类型原型为原型
  prototype.constructor = subType; // 增强对象,将构造器指向子类型
  subType.prototype = prototype; // 子类型的原型指向新创建的对象
}

function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function () {
  return this.property;
};

function SubType() {
  SuperType.call(this); // 继承了SuperType,同时还保存了构造函数链
}

inheritPrototype(SubType, SuperType); // 寄生式继承

SubType.prototype.getSubValue = function () {
  return this.property && !this.getSuperValue();
};

var instance1 = new SubType();
console.log(instance1.getSuperValue()); // true
console.log(instance1.getSubValue()); // false

在这个例子中,createObject函数用于创建一个新对象,其原型为传入的对象。inheritPrototype函数用于实现寄生式继承,它首先通过createObject创建了一个新对象,这个新对象的原型是父类型的原型,然后增强这个对象,将其构造器指向子类型,最后让子类型的原型指向这个新创建的对象。这样,子类型就继承了父类型的属性和方法,同时还可以在子类型的原型上添加新的方法。

69.什么是栈内存,什么是堆内存?

栈内存:是自动分配的内存区域,主要用于存储基本数据类型和对象的引用(而非对象本身)。在 JavaScript 中,局部变量(包括函数参数)就是栈内存中分配的。栈内存有一个重要的特性,既它是按照后进先出(LIFO)的原则进行管理的。当定义一个变量时,它会在栈内存中占据一定的空间,而当该变量不在需要时(例如函数执行完毕后),其占用的空间会自动被释放。这种自动的内存管理使得栈内存的操作非常高效且错误率较低。

堆内存:用于动态分配内存区域,主要用于存储对象(包括数组和函数)。在 JavaScript 中,当使用 new 关键字创建一个对象时,这个对象就会被分配在堆内存中。与栈内存不同,堆内存的对象生命周期是由 JavaScript 的垃圾收集机制来管理的。当没有任何引用指向一个对象时,垃圾收集器会将其标记为可回收,并在适当的

70.JS 如何实现多线程?

JavaScript 通常被视为单线程语言,因为它只有一个主线程来处理所有的任务。这意味着 JavaScript 代码在任何给定的时间只能执行一个任务。然而,JavaScript 提供了一些机制,使得它能够在某种程度上实现多线程的效果。以下是一些实现方式:

  1. Web Workers: Web Workers 是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。它们被设计为在 Web 内容在用户的浏览器上运行时运行后台任务。Web Workers 是运行在浏览器中的一个独立线程,它们不能访问 DOM,但可以通过 postMessage/onmessage 方法与主线程进行通信。
    创建 Web Worker 的步骤:
  • 创建一个新的 Worker 对象,指向一个 JavaScript 文件。
  • 使用 postMessage 方法向 Worker 发送数据。
  • 在 Worker 内部,监听 message 事件以接收数据,然后处理它。
  • 使用 Worker 的 onerror 事件处理程序来处理任何错误。
  1. SharedArrayBuffer 和 Atomics: 这两个 API 允许在多个 Worker 之间共享内存,并且提供了原子操作来确保内存访问的同步。SharedArrayBuffer 提供了一个用于存储固定长度的原始二进制数据的缓冲区,而 Atomics 提供了一种方法来以原子方式读取和写入 SharedArrayBuffer。

  2. Promise 和 async/await: 虽然它们并不直接提供多线程,但它们提供了一种编写异步代码的方式,可以避免阻塞主线程。Promise 对象用于表示一个异步操作的最终完成(或失败)及其结果值。async/await 是基于 Promise 的语法糖,使得异步代码的书写和理解更接近同步代码。时机释放其占用内存。

71.Proxy 可以实现什么功能?

  1. 属性访问控制:你可以通过Proxy拦截对象属性的读取和写入操作。例如,你可以实现属性的私有性,或者在属性被访问或修改时执行某些操作。
  2. 数据验证:在对象属性被赋值时,你可以使用Proxy进行数据的验证。如果数据不符合预期,你可以阻止赋值操作。
  3. 日志和调试 :你可以使用Proxy来记录对象属性的访问和修改历史,这对于调试和性能分析非常有用。
  4. 非侵入式操作Proxy允许你在不修改原始对象的情况下,对对象的行为进行扩展或修改。这对于第三方库或框架来说特别有用,因为它们可以在不改变原始代码的情况下添加新功能。
  5. 函数拦截:除了属性访问,Proxy还可以拦截函数调用,这使得你可以在执行函数前后添加自定义逻辑。
  6. 默认行为:即使你拦截了某些操作,Proxy也提供了默认的行为,你可以在必要时调用它们。例如,如果你拦截了属性查找操作但没有提供替换行为,那么Proxy会回退到默认的属性查找行为。

72.解释 JavaScript eval() 是做什么的?

eval() 是 JavaScript 中的一个内置函数,它的主要作用是将传入的字符串作为 JavaScript 代码进行解析和执行。

当你有一个字符串,而这个字符串实际上是有效的 JavaScript 代码时,你可以使用 eval() 来执行这段代码。例如:

let code = 'console.log("Hello, world!")';
eval(code); // 输出 "Hello, world!"

然而,尽管 eval() 有这样的功能,但在实际开发中,我们通常建议避免使用它,原因主要有以下几点:

  1. 安全性问题:eval() 执行的是任意字符串作为 JavaScript 代码,这可能导致安全隐患。如果执行的代码来自于不可信的源,那么它可能包含恶意代码,如尝试访问或修改其他对象,或者执行其他非法操作。
  2. 性能问题:eval()的执行速度通常比其他 JavaScript 代码慢,因为它需要 JavaScript 引擎去解析和执行字符串中的代码。
  3. 调试困难:当使用 eval()`时,如果在执行的代码中发生错误,那么错误追踪和调试可能会变得非常困难。
  4. 代码可读性:使用 eval()会使得代码更难阅读和理解,因为它隐藏了实际的执行逻辑。

因此,除非你非常清楚你正在做什么,并且确信你正在执行的代码是安全的,否则最好避免使用 eval()。在大多数情况下,都有更安全、更高效的替代方案,比如使用函数、对象和方法来组织你的代码。

73.说一说服务端渲染及优势?

在 JavaScript 中,服务端渲染(Server-Side Rendering,简称 SSR)是一种页面渲染技术,指在服务端(即服务器)完成页面的渲染工作,生成完整的 HTML 页面,然后将这个渲染好的页面直接发送给客户端(即用户的浏览器)。

优点:

  1. 搜索引擎优化(SEO):搜索引擎爬虫能够直接解析和理解服务端渲染的 HTML 内容。由于爬虫通常只能抓取到页面的静态内容,而无法执行 JavaScript 代码,因此服务端渲染的页面更易于被搜索引擎识别和索引,从而提高网站在搜索结果中的排名。
  2. 首屏加载性能:在客户端渲染中,浏览器需要先下载并执行 JavaScript 代码,然后才能生成和渲染页面内容。而服务端渲染可以直接在服务器端生成完整的 HTML 响应,减少了客户端的处理时间,因此可以更快地提供页面内容给用户,提高了首屏加载的速度。
  3. 改善了用户体验:由于服务端渲染可以更快地提供内容,用户等待时间变短,页面白屏时间减少,从而改善了用户体验。此外,由于服务端已经生成了页面的初始状态,用户可以立即与页面进行交互,而无需等待 JavaScript 的下载和执行。
  4. 分担客户端压力:在客户端渲染中,大量的计算和渲染工作需要在用户的设备上完成,这可能对设备性能造成一定的压力。而服务端渲染将这些工作转移到服务器上,从而减轻了客户端设备的负担。

缺点:

  1. 增加了服务端的资源消耗和维护成本;
  2. 同时不利于前后端分离,需要前端来维护一个模板层

74.简述 attribute 和 property 的区别 ?

定义不同
attribute 是 HTML 标签上的特性,它的值只能够是字符串;
property 是 DOM 中的属性,是 JavaScript 里的对象。

获取方式不同:
attribute 通过 getAttribute()方法获取;
property 通过点符号(.)或方括号([])来访问;
包含内容不同:
attribute 包含的是 HTML 元素上的附加信息,用于提供元素的更多描述和行为;
property 包含的是对象的状态或数据,并可以通过访问器方法(getter 和 setter)来控制对属性的读取和修改。

兼容性: document.ready 是 jQuery 库中的一个事件,而非原生 JavaScript 的一部分。在不这意味着使用 jQuery 的情况下,你将无法使用 document.ready。相比之下,document.onload 是原生 JavaScript 的一部分,具有更好的兼容性。

75.请指出 document.onload 和 document.ready 两个事件的区别?

触发时间: document.onload 事件在整个页面(包括所有图片、样式表、脚本等)都完全加载完毕后才会触发。这意味着,如果页面中有大量资源需要加载,用户可能需要等待一段时间才能看到 document.onload 事件触发的效果。相比之下,document.ready 事件在 DOM(Document Object Model,文档对象模型)结构绘制完成后就会触发,不必等待所有的外部资源如图片和样式表加载完成。因此,一般来说,document.ready 的触发时间要早于 document.onload。

用途: document.ready 更适合于需要在 DOM 结构绘制完成后立即执行的代码,例如修改页面元素的样式或绑定事件处理器等。而 document.onload 更适合于需要等待所有资源都加载完成后再执行的代码,例如需要图片资源才能正确显示的动画效果等。

76.说一下 token 能放在 cookie 中吗?

Token 可以放在 Cookie 中。Token 一般是用来判断用户是否登录的,它内部包含的信息有:用户唯一的身份标识(uid)、当前时间的时间戳(time)以及签名(sign)。Token 的存在本身只关心请求的安全性,而不关心 Token 本身的安全,因为 Token 是服务器端生成的,可以理解为一种加密技术。然而,将 Token 存储在 Cookie 中虽然可以自动发送,但存在不能跨域的问题,且如果 Cookie 内存放 Token,浏览器的请求默认会自动在请求头中携带 Cookie,容易受到 CSRF 攻击。因此,将 Token 存放在 Cookie 中时,不应设置 Cookie 的过期时间,且 Token 是否过期应由后端来判断。如果 Token 失效,后端应在接口中返回固定的状态表示 Token 失效,需要重新登录,并在重新登录时重新设置 Cookie 中的 Token。

总的来说,虽然可以将 Token 放在 Cookie 中,但需要注意相关的安全问题,并谨慎处理 Token 的过期和重新登录逻辑。在实际应用中,也可以考虑将 Token 存放在其他更安全的地方,如 localStorage 或 sessionStorage。

77.什么是长链接,的作用、用法和使用场景?

在 JavaScript 中,长链接(也称为持久连接、keep-alive 连接或连接保持)是一种通信机制,它允许客户端和服务器在一个连接上发送多个请求和响应,而无需为每个请求/响应对创建新的连接。这种机制显著降低了服务器的负载,提高了资源的使用率。

作用

  1. 性能提升:通过复用同一个连接,长链接减少了频繁建立和关闭连接的开销,从而提高了应用的性能。
  2. 实时性增强:长链接适用于需要实时数据更新的场景,因为它允许服务器主动推送数据到客户端,无需客户端频繁轮询。
  3. 资源节约:由于减少了连接建立和断开的次数,长链接也节约了网络资源。

用法

在 JavaScript 中,你可以通过以下方式使用长链接:

  1. 使用XMLHttpRequest或Fetch API:这两个 API 在发送 HTTP 请求时默认使用长连接。当使用它们时,你无需额外配置即可享受长链接带来的好处。
  2. WebSocket:WebSocket 是另一种实现长链接的方式。它提供了一个全双工的通信通道,允许服务器和客户端之间实时地交换数据。
  3. Server-Sent Events (SSE):SSE 是一种轻量级的、单向的长链接技术。它允许服务器向客户端推送事件流,通常用于实时更新或通知。

使用场景

  1. 实时通信应用:如在线聊天室、即时消息应用等,需要实时传输文本、图片、音频和视频等信息。
  2. 实时数据更新:如股票价格、天气预报、新闻推送等需要实时更新的应用。
  3. 协作工具:如在线文档编辑、实时协作工具等,需要多个用户实时共享和编辑数据。
  4. 游戏:在线多人游戏通常需要实时通信来同步玩家状态、位置和其他游戏数据。

需要注意的是,虽然长链接带来了很多好处,但在某些场景下可能并不适用。例如,对于请求频率较低或数据量较小的应用,使用短连接可能更为合适。此外,长链接也需要额外的管理,以确保连接的稳定性和安全性。因此,在选择使用长链接还是短连接时,需要根据具体的应用需求和场景进行权衡。

78.在javascript中什么是短链接,优缺点是什么,应用场景和长链接有什么区别,怎么相互转换?

在JavaScript的上下文中,短链接通常不是指某种特定的网络连接方式,而是指网址或URI(统一资源标识符)的缩短版本。这些短链接通常由一些专门的URL缩短服务生成,比如bit.ly, tinyurl等,或者是应用程序内部实现的短URL生成逻辑。它们被设计用来将长URL转换为更简洁、更易于分享或嵌入的格式。

短链接的优点

  1. 长度简短:易于分享、打印和记忆。
  2. 美观:在一些界面上,短链接可能看起来更整洁。
  3. 隐藏原始URL:可以用于隐藏原始URL的复杂性或敏感信息。
  4. 统计和跟踪:一些URL缩短服务提供了点击统计和跟踪功能,可以帮助分析用户行为。

短链接的缺点

  1. 可靠性问题:如果缩短服务关闭或不可用,链接可能会失效。
  2. 跳转延迟:用户点击短链接后,需要经过一次或多次重定向才能到达目标页面,可能会产生延迟。
  3. 安全风险:有时短链接可能被用于恶意目的,如钓鱼攻击或传播恶意软件。

应用场景

  1. 社交媒体分享:在Twitter等字符限制严格的平台上,短链接非常有用。
  2. 移动应用:在有限的界面空间内,短链接更容易显示。
  3. 电子邮件营销:为了保持邮件的整洁和避免被标记为垃圾邮件,可以使用短链接。
  4. 广告和推广:用于跟踪广告点击和效果。

长链接与短链接的区别
长链接是指完整的、原始的URL,它通常较长且包含了目标资源的所有必要信息。
短链接则是这个长链接的缩短版本。

两者主要区别在于长度和外观,但更重要的是,长链接通常直接指向目标资源,而短链接则需要经过一次多次重定向才能到达目标。

相互转换

长链接转短链接

  1. 使用URL缩短服务:注册并登录到一个URL缩短服务(如bit.ly),然后输入你想要缩短的长链接,服务会为你生成一个短链接。
  2. 自定义短链接:一些服务允许你自定义短链接的后缀部分,以便更好地与你的品牌或内容匹配。
  3. 程序生成:你也可以自己编写代码,通过一定的算法将长链接转换为短链接。这通常涉及到哈希函数和数据库存储。

短链接转长链接

  1. 直接访问:在浏览器中直接访问短链接,浏览器会自动跟随重定向并最终到达长链接指向的目标页面。
  2. API查询:如果你使用的是某个特定的URL缩短服务,并且该服务提供了API,你可以通过API查询短链接对应的长链接。
  3. 解析重定向:你也可以编写代码来模拟浏览器的行为,通过解析短链接的重定向链来找到最终的长链接。

需要注意的是,由于重定向和额外的解析步骤,使用短链接可能会稍微增加一些网络延迟和复杂性。因此,在不需要缩短链接的情况下,直接使用长链接通常是更好的选择。

79.WEB 应用从服务器主动推送 Data 到客户端有哪些方式?

  1. 轮询(Polling):客户端定期向服务器发送请求,检查是否有新的数据可用。这实际上并不是服务器端主动推送数据,而是客户端主动查询。轮询方式简单但效率低下,因为服务器可能在大部分时间里都没有新的数据,但客户端仍然需要不断地发送请求。

  2. 长轮询(Long Polling):客户端向服务器发送请求后,服务器会保持连接打开,直到有新的数据可用或者连接超时。一旦有数据,服务器会立即返回响应给客户端。这种方式相比普通轮询减少了无效请求的次数,但仍然不是真正的服务器主动推送。

  3. WebSocket:WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它允许服务器主动向客户端推送数据,而无需客户端发送请求。WebSocket 使得客户端和服务器之间的数据交换变得更加简单和高效。

  4. Server-Sent Events(SSE):SSE 允许服务器向客户端推送更新。与 WebSocket 不同,SSE 是单向的,只能从服务器发送到客户端。SSE 基于 HTTP 协议,使用文本/事件流 MIME 类型,可以通过简单的 JavaScript API 在浏览器中实现。

  5. HTTP/2 服务器推送:HTTP/2 协议支持服务器推送功能,允许服务器在客户端请求某个资源时,主动推送其他相关资源到客户端。这可以减少客户端的请求次数,提高加载速度。但需要注意的是,HTTP/2 的服务器推送需要客户端的支持,并且不是所有场景都适合使用。

  6. Web Push Notifications:Web Push Notifications 是一种基于浏览器和服务器之间的长连接实现的推送通知机制。用户可以通过订阅一个站点的 Web Push 服务,即使关闭了浏览器,一旦站点发送了推送消息,用户也能收到通知。这种方式主要用于发送通知性质的消息,而不是实时数据更新。

80.简述异步线程、轮询机制、宏任务微任务?

异步线程、轮询机制、宏任务和微任务在编程中各自有不同的使用场景和原理。

异步线程的使用场景与原理

使用场景:异步线程主要用于解决线程阻塞和响应慢的问题,特别适用于 I/O 操作(如文件读写、网络数据修改、数据库操作等)以及跨进程的调用(如 Web Service、HttpRequest 以及.Net Remoting 等)。这些操作通常耗时较长,如果采用同步方式执行,会阻塞主线程,影响程序的响应性和性能。

原理:异步线程通过将耗时任务交给其他线程或后台执行,使得主线程可以继续执行后续任务而不被阻塞。当后台任务执行完毕后,通过回调函数或 Promise 等方式通知主线程任务已经完成,主线程则执行相应的回调函数或解决 Promise 以进行后续处理。

轮询机制的使用场景与原理

使用场景:轮询机制广泛应用于各种工业自动化、过程控制、数据采集与处理等领域,如工厂生产线自动化控制、环境监测系统、医疗设备监控和楼宇自控系统等。这些系统通常需要实时获取设备的状态或数据,以便及时发现问题和处理。

原理:轮询机制通过 CPU 定时发出询问,依序询问每一个周边设备是否需要其服务。如果有设备需要服务,CPU 则提供相应的服务;服务结束后,再询问下一个设备,如此周而复始。在客户端-服务器架构中,轮询机制体现为客户端定时向服务器发送请求以获取最新数据。

宏任务和微任务的使用场景与原理

使用场景

  • 宏任务:通常用于执行耗时的操作,如长时间运行的计算任务或需要等待 I/O 操作完成的任务。
  • 微任务:主要用于确保执行顺序的一致性,例如在 if-else 语句中,如果其中一个分支是微任务,另一个不是,使用微任务可以确保程序的一致性执行。此外,微任务也用于批量操作,通过将从不同来源的请求收集到单一的批处理中,避免对处理同类工作的多次调用可能造成的开销。

原理:在 JavaScript 的事件循环中,宏任务和微任务按照特定的顺序执行。每个宏任务执行完毕后,会立即执行所有等待中的微任务。这种机制确保了微任务总是优先于后续的宏任务执行,从而实现了对异步处理逻辑的精细控制。

总的来说,异步线程、轮询机制、宏任务和微任务都是处理并发和异步操作的重要工具,它们各自在不同的场景和需求下发挥着作用,帮助开发者构建高效、响应性良好的应用程序。

81.JavaScript 中的负无穷大是什么?

在 JavaScript 中,负无穷大(Negative Infinity)是一个特殊的浮点数值,它表示比任何可表示的负数都要小的值。当你尝试将一个负数除以零时,JavaScript 会返回负无穷大。负无穷大在 JavaScript 中是一个特殊的值,你可以使用 Number.NEGATIVE_INFINITY 来访问它。

以下是一个示例,展示了如何得到负无穷大:

let num = -1 / 0;
console.log(num); // 输出: -Infinity
console.log(Number.NEGATIVE_INFINITY === num); // 输出: true

在这个例子中,num 的值被设置为负无穷大,因为它是一个负数除以零。然后我们使用 Number.NEGATIVE_INFINITY 来检查 num 是否真的等于负无穷大,结果确实如此。

在比较运算中,负无穷大小于任何其他数值,包括负数和零。同时,负无穷大也小于正无穷大 (Number.POSITIVE_INFINITY)。

82.JS 中什么是垃圾回收机制?有什么好处?

在 JavaScript 中,垃圾回收机制是一种自动内存管理的过程,它负责跟踪和释放不再使用的对象所占用的内存。这种机制使得开发者无需手动管理内存,从而减少了内存泄漏和内存管理错误的风险。

JS 中的垃圾回收机制
JavaScript 的垃圾回收机制主要依赖于两种策略:标记-清除(Mark-and-Sweep)和分代收集(Generational Collection)。

  1. 标记-清除(Mark-and-Sweep)

    • 垃圾回收器从根对象(如全局对象)开始,递归地访问对象的属性,并为这些对象加上标记。
    • 然后,它会遍历整个堆内存,找出那些没有被标记的对象,这些对象就是不再被引用的对象,因此可以被回收。
  2. 分代收集(Generational Collection)

    • 这种策略基于一个假设:很多对象都是“朝生夕死”的,即它们很快就会被回收。
    • 因此,垃圾回收器将内存划分为新生代和老生代,对新生代更频繁地进行垃圾回收,而对老生代则较少进行垃圾回收。

好处

  • 简化内存管理:开发者无需关心内存的分配和释放,可以专注于实现业务逻辑。

  • 减少错误:手动管理内存时,很容易出现内存泄漏或野指针等问题。自动垃圾回收机制大大减少了这类错误的可能性。

  • 优化性能:垃圾回收器通常经过高度优化,可以高效地回收不再使用的内存,从而确保程序的高效运行。

  • 跨平台一致性:无论你的代码在哪个 JavaScript 引擎上运行,都可以期望有相似的内存管理行为,这有助于跨平台开发的一致性。

以下是一些常见的导致内存泄漏的情况:

  • 全局变量的不当使用。
  • 闭包中的循环引用。
  • DOM 元素的引用未释放。
  • 定时器或回调未清除。

83.说一说 HashRouter 和 HistoryRouter 的区别和原理?

  1. URL 表现形式:
  • HashRouter 模式下,地址栏会带有#号,其后跟随路由路径。这种形式的 URL 不会发送给服务器,因此不会重新加载页面。
  • HistoryRouter 模式下,地址栏不会带有#号或其他特殊符号,看起来更加美观和优雅。它利用 HTML5 的 History API 进行 URL 操作,可以模拟传统的页面跳转。
  1. 工作原理:
  • HashRouter 的工作原理依赖于浏览器的 hashchange 事件。当 URL 的 hash 部分(即#号后面的部分)发生变化时,会触发 hashchange 事件。HashRouter 通过监听这个事件来捕获 URL 的变化,并根据新的 hash 值查找对应的路由规则,从而展示相应的页面内容。由于 hash 变化不会触发页面刷新,因此 HashRouter 可以实现无刷新的页面跳转。
  • HistoryRouter 则利用 HTML5 的 History API 中的 pushState()和 replaceState()方法来实现 URL 的变化。这些方法可以在不重新加载页面的情况下修改浏览器的历史记录。同时,HistoryRouter 还使用 window 对象的 onpopstate 事件来监听浏览器的前进和后退操作。当用户使用浏览器的后退或前进按钮时,onpopstate 事件会被触发,HistoryRouter 会根据当前的历史记录状态来查找对应的路由规则并展示相应的页面内容。

84.什么是防抖和节流?有什么区别?如何实现?

在 JavaScript 中,防抖(debounce)和节流(throttle)是两种常用的优化高频率触发事件的技术,它们都可以用于限制函数的执行频率,但实现方式和应用场景有所不同。

防抖(Debounce)
防抖是指在事件被触发后 n 秒内函数只能执行一次,如果在这 n 秒内又被重新触发,则重新计算执行时间。它的主要应用场景是:一些需要用户持续输入的场景,如搜索框实时搜索、表单验证等。

实现防抖的基本思路是:设定一个定时器,在事件被触发时重置定时器,如果定时器执行了,就执行函数。

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

节流(Throttle)
节流是指在一定时间内只执行一次函数,无论事件被触发多少次。它的主要应用场景是:一些需要限制执行频率的场景,如滚动加载、窗口大小调整等。

实现节流的基本思路是:设定一个时间间隔,在这个时间间隔内,无论事件被触发多少次,都只执行一次函数。

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

区别
防抖和节流的主要区别在于它们对事件触发频率的处理方式。防抖是在事件被触发后等待一段时间再执行函数,而节流是在一定时间内只执行一次函数。因此,防抖更注重事件的结束,而节流更注重事件的开始。

85.说一说跨域是什么?如何解决跨域问题?

跨域 是指浏览器在执行一个脚本时,由于该脚本所在的源(协议、域名、端口)与目标资源所在的源不一致,而浏览器为了安全起见,限制脚本访问其他源的资源。这种安全机制称为同源策略,是浏览器对 JavaScript 等脚本语言施加的安全限制。

跨域问题的出现,主要是因为浏览器为了防止恶意脚本攻击而设定的安全限制。例如,如果 A 网站的脚本试图访问 B 网站的资源,但 A 和 B 的协议、域名或端口不同,那么浏览器的同源策略就会阻止这一访问行为。

解决跨域的方法:

  1. JSONP:JSONP 是一种非官方的跨域数据交互协议。它利用<script>标签没有跨域限制的漏洞,通过动态创建<script>标签,以获取其他来源的 JavaScript 文件形式,使用回调函数的形式实现跨域数据访问。但 JSONP 只支持 GET 请求。
  2. CORS:CORS 是一种官方的跨域解决方案,它通过在服务器端设置响应头信息来告诉浏览器,允许哪些源来访问资源。这样,浏览器在发送请求时,会检查这些响应头信息,从而决定是否允许跨域访问。CORS 支持各种 HTTP 请求方法,如 GET、POST 等。
  3. 代理服务器:通过在服务端设置一个代理服务器来转发请求,使得浏览器端的请求看似是同源的,从而绕过浏览器的同源策略限制。这种方法需要前后端配合,后端需要编写相应的代理逻辑。
  4. 使用 postMessage 和 message 事件:HTML5 引入了 window.postMessage 方法来安全地实现跨源通信。通过这个方法,你可以向其他的 window 对象发送数据,无论这个 window 对象是否与当前的脚本同源。同时,目标 window 对象可以通过监听’message’事件来接收数据。

86.打印控制台,出现栈溢出是什么情况?

在 JavaScript 中,当您看到控制台中报告栈溢出(Stack Overflow)错误时,这通常意味着您的代码在执行过程中创建了一个无限递归或一个非常深的调用栈。JavaScript 引擎有一个限制,即调用栈的大小是有限的,当达到这个限制时,就会抛出栈溢出错误。递归是一个常见的导致栈溢出的原因。当递归函数没有正确的退出条件,或者退出条件设置得不合理,导致函数无限次地调用自身时,就会发生栈溢出。

例如,下面的递归函数会导致栈溢出,因为它没有正确的退出条件:

function infiniteRecursion() {
  infiniteRecursion(); // 没有退出条件,无限递归
}

infiniteRecursion(); // 调用这个函数会导致栈溢出

另外,某些非递归操作,如深度嵌套循环或者函数内连续调用大量其他函数,如果层次太深,也可能导致栈溢出。

避免栈溢出:

  1. 确保递归函数有正确的退出条件。
  2. 避免创建过深的调用栈。
  3. 使用迭代而非递归,如果可能的话。
  4. 分析代码,查找可能导致调用栈过深的逻辑,并进行优化。

87.说一下有什么方法可以保持前后端实时通信?

  1. 轮询:客户端设置定时器,每隔一段时间向服务端发送请求,通过频繁请求达到实时效果。但这种方式会消耗较多流量和 CPU 利用率,且轮询间隔不好控制。
  2. 长轮询:客户端和服务端保持一条长连接,服务端有新数据时主动发送给客户端。这种方式对服务器的高并发能力有要求。
  3. WebSocket:一种全双工通信协议,客户端和服务端处于相同地位,可以实时双向通信。
  4. SSE(Server-Sent Event):服务端与客户端建立的单向通道,只能由服务端传输特定形式的数据给客户端。
  5. iframe 流:在页面中插入一个隐藏的 iframe,利用其 src 属性在服务器和客户端之间创建一条长连接,服务器向 iframe 传输数据来实时更新页面。

88.如何解决移动端 HTML5 音频标签 audio 的 autoplay 属性失效问题?

  1. 使用交互触发播放:最直接的方法是在用户进行某些交互(如点击按钮)后再播放音频。这种方法通常能得到最好的兼容性和用户体验。
<button onclick="playAudio()">播放音频</button>
<script>
  function playAudio() {
    var audio = document.getElementById("myAudio");
    audio.play();
  }
</script>
  1. 使用静音属性:在某些情况下,如果你将音频设置为静音,那么浏览器可能会允许自动播放。然后,你可以通过其他方式(如使用 JavaScript)控制音量。
<audio id="myAudio" autoplay muted>
  <source src="audiofile.mp3" type="audio/mpeg" />
</audio>
  1. 监听页面可见性变化:你可以监听 visibilitychange 事件,当页面变为可见时尝试播放音频。
document.addEventListener("visibilitychange", function () {
  if (document.visibilityState === "visible") {
    var audio = document.getElementById("myAudio");
    audio.play().catch(function (error) {
      console.log("播放失败: ", error);
    });
  }
});
  1. 使用服务工作者:在某些情况下,你可以尝试使用服务工作者(Service Worker)来在后台播放音频。然而,这种方法可能并不总是有效,并且可能增加实现的复杂性。
  • 29
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值