【前端面试题】:JavaScript篇(1)

  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 的区别和原理?

下面是我在学习HTML和CSS的时候整理的一些笔记,有兴趣的可以看下:

HTML、CSS部分截图

进阶阶段

进阶阶段,开始攻 JS,对于刚接触 JS 的初学者,确实比学习 HTML 和 CSS 有难度,但是只要肯下功夫,这部分对于你来说,也不是什么大问题。

JS 内容涉及到的知识点较多,看到网上有很多人建议你从头到尾抱着那本《JavaScript高级程序设计》学,我是不建议的,毕竟刚接触 JS 谁能看得下去,当时我也不能,也没那样做。

我这部分的学习技巧是,增加次数,减少单次看的内容。就是说,第一遍学习 JS 走马观花的看,看个大概,去找视频以及网站学习,不建议直接看书。因为看书看不下去的时候很打击你学下去的信心。

然后通过一些网站的小例子,开始动手敲代码,一定要去实践、实践、实践,这一遍是为了更好的去熟悉 JS 的语法。别只顾着来回的看知识点,眼高手低可不是个好习惯,我在这吃过亏,你懂的。

1、JavaScript 和 ES6

在这个过程你会发现,有很多 JS 知识点你并不能更好的理解为什么这么设计,以及这样设计的好处是什么,这就逼着让你去学习这单个知识点的来龙去脉,去哪学?第一,书籍,我知道你不喜欢看,我最近通过刷大厂面试题整理了一份前端核心知识笔记,比较书籍更精简,一句废话都没有,这份笔记也让我通过跳槽从8k涨成20k。

JavaScript部分截图

2、前端框架

前端框架太多了,真的学不动了,别慌,其实对于前端的三大马车,Angular、React、Vue 只要把其中一种框架学明白,底层原理实现,其他两个学起来不会很吃力,这也取决于你以后就职的公司要求你会哪一个框架了,当然,会的越多越好,但是往往每个人的时间是有限的,对于自学的学生,或者即将面试找工作的人,当然要选择一门框架深挖原理。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

以 Vue 为例,我整理了如下的面试题。

Vue部分截图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值