JavaScript 、 ES6 部分

整理不易,家人们不想开通 VIP ,可以私我,有偿(4 yuan 购入PDF版)有更新都会踢。
【狗头保命,小的认真总结的,大部分都是我面试遇到的,感谢理解】

更新日志

2024年8月29日更新 —— ES6新特性有哪些?
2024年8月30日更新——xmlhttpRequest的解释
2024年8月31日更新——

  • 防抖和节流的应用场景?
  • 在开发过程中,原型链的应用场景?
  • 箭头函数的作用
  • 在JS中,怎么样实现面向对象的编程

前端八股文

1.【HTML + HTML5】前端八股文面试题
2.【CSS + CSS3】前端八股文面试题
3.【JavaScript + ES6】前端八股文面试题
4.【jQuery】前端八股文面试题
5.【Vue2 + Vue3】前端八股文面试题
6.【Uniapp】前端面试题
7.【HTTP、Web常用协议等等】前端八股文面试题
8.【实例总结】前端八股文面试

1. ===和==的区别

在 JavaScript 中, == 和 === 都是用于比较操作的运算符。

但它们在比较规则上有明显的区别:

  • == (相等运算符):在进行比较时会进行类型转换,然后比较值是否相等。

例如: ‘5’ == 5 会返回 true ,因为会将字符串 ‘5’ 转换为数字 5 进行比较。

  • === (严格相等运算符):不仅比较值,还比较类型。只有值和类型都相同时,才返回 true 。

例如: ‘5’ === 5 会返回 false ,因为类型不同,一个是字符串,一个是数字。

[]===[] //false
{}==={} //false
null ===null  // true
{}=={} // false
以下是一些 JavaScript 中关于比较判断的经典例题:
 
例题 1:
let num1 = 5;
let num2 = '5';

console.log(num1 == num2);  //true
console.log(num1 === num2);  //false
 
此题主要考察  ==  和  ===  在面对不同数据类型时的比较结果。
 
例题 2:
let obj1 = { name: 'John' };
let obj2 = { name: 'John' };

console.log(obj1 == obj2);  //false
console.log(obj1 === obj2);  //false
 
此例考查对象的比较,由于对象是引用类型,即使属性相同,两个不同的对象引用也不会相等。
 
例题 3:
let arr1 = [1, 2, 3];
let arr2 = [1, 2, 3];

console.log(arr1 == arr2); //false
console.log(arr1 === arr2); //false

此例针对数组的比较,与对象类似,即使元素相同,不同的数组引用也不相等。
 
例题 4:
let num = null;
console.log(num == undefined);  //true
console.log(num === undefined);  //false
 
此例涉及  null  和  undefined  的比较。
 
例题 5:
let str1 = 'hello';
let str2 = new String('hello');

console.log(str1 == str2);  //true
console.log(str1 === str2);  //false
 
此例考察字符串直接量和通过  String  构造函数创建的字符串对象的比较。

2. for 循环 var 定义和多个变量执行的次数

  • 在 for 循环中使用 var 定义变量时,会存在变量提升的问题,即变量会在函数作用域的顶部声明。
  • 多个变量的执行次数取决于循环的条件。
例如,以下代码中:
for (var i = 0; i < 10; i++) {
  for (var j = 0; j < 6; j++) {
    // 循环体中的代码
  }
}

外层循环会执行 10 次,内层循环会在外层循环的每一次迭代中执行 6 次。因此,总的执行次数为 10 * 6 = 60 次。

【注意】由于 var 定义的变量在整个函数作用域内都是可见的,可能会导致一些意外的结果。
在 ES6 中,推荐使用 let 或 const 来定义变量,它们具有块级作用域,可以避免这些问题。

3.什么是闭包?

在 JavaScript 中,闭包(Closure)是指有权访问另一个函数作用域中的变量的函数。

1)形成闭包的条件
  • 存在一个内层函数。
  • 内层函数引用了外层函数的变量。
  • 外层函数将内层函数作为返回值或者传递给其他地方进行调用。
2)示例解释

以下是一个简单的闭包示例:

function outerFunction() {
  const outerVariable = "I am from outer function";
  function innerFunction() {
    console.log(outerVariable);
  }
  return innerFunction;
}

const closureFunction = outerFunction();
closureFunction(); // 输出:I am from outer function

在这个例子中:

  • outerFunction是外层函数,它定义了一个变量outerVariable。
  • innerFunction是内层函数,它引用了outerVariable。
  • outerFunction返回了innerFunction,当把这个返回值赋给closureFunction并调用closureFunction时,innerFunction仍然能够访问到outerVariable,这就形成了闭包。
3)闭包的作用
  • 数据封装和隐藏:可以创建私有变量,避免全局命名空间的污染。例如:
   function createCounter() {
     let count = 0;
     return {
       increment() {
         count++;
         return count;
       },
       decrement() {
         count--;
         return count;
       },
     };
   }

   const counter = createCounter();
   console.log(counter.increment()); // 1
   console.log(counter.increment()); // 2

在这个例子中,count变量只能通过increment和decrement方法访问,外部无法直接修改count,实现了数据的封装和隐藏。

  • 模拟私有方法:在面向对象编程中,可以使用闭包来模拟私有方法。例如:
    const myObject = (function () {
      let privateVariable = "private value";
      function privateMethod() {
        console.log(privateVariable);
      }
      return {
        publicMethod() {
          privateMethod();
        },
      };
    })();
    
    myObject.publicMethod(); // 输出:private value
    

privateVariable和privateMethod只能在创建myObject的立即执行函数内部访问,外部无法直接访问,模拟了私有变量和私有方法。

  • 函数记忆化:闭包可以用于缓存函数的结果,避免重复计算。例如:
    function memoizedAddTo80() {
      let cache = {};
      return function (n) {
        if (n in cache) {
          return cache[n];
        } else {
          console.log("long time");
          cache[n] = n + 80;
          return cache[n];
        }
      };
    }
    
    const memoized = memoizedAddTo80();
    console.log(memoized(5)); // long time,输出:85
    console.log(memoized(5)); // 输出:85(直接从缓存中获取结果)
    

在这个例子中,闭包保存了cache对象,用于存储已经计算过的结果,下次调用相同的参数时可以直接从缓存中获取结果,提高了函数的性能。

4)注意事项
  • 闭包可能会导致内存泄漏:如果闭包引用了一个不再需要的变量,而这个变量又没有被正确地释放,就可能会导致内存泄漏。因此,在使用闭包时,要确保不再需要的变量能够被正确地垃圾回收。
  • 闭包可能会影响性能:由于闭包会保留对外部变量的引用,这可能会导致一些性能问题,特别是在处理大量闭包或复杂的闭包结构时。因此,在使用闭包时,要注意性能优化,避免不必要的闭包使用。

以下是一个示例代码,用于给大家判断哪一个是闭包:

function outer() {
  var outerVar = '外层变量';
  function inner() {
    console.log(outerVar);
  }
  return inner;
}

var closure = outer();
closure(); 

在上述代码中, inner 函数能够访问外部函数 outer 中的 outerVar 变量,因此 inner 函数是一个闭包。

4.async返回的是什么类型?

在 JavaScript 中, async 函数总是返回一个 Promise 对象。

  • 如果 async 函数内部的执行逻辑通过 return 语句返回了一个值,那么这个 Promise 对象最终会被解析为这个返回的值;
  • 如果函数内部抛出了异常或者发生了错误,那么这个 Promise 对象最终会被拒绝,并携带抛出的错误信息。
例如:
async function myAsyncFunction() {
  return 5; 
}

myAsyncFunction().then(result => console.log(result)); 
// 输出 5

在这个例子中, myAsyncFunction 函数返回的 Promise 对象最终被解析为数字 5 。

5.箭头函数能不能return

箭头函数可以返回一个值。

  • 如果函数体只有一条表达式,可以省略 {} 和 return 关键字,表达式的值会被自动返回。
  • 如果函数体包含多条语句或需要显式返回一个值,则需要使用 {} 括起来,并使用 return 关键字。
例如:
const sum = (a, b) => a + b;  // 自动返回表达式的值

const multiply = (a, b) => {
  return a * b;  // 显式使用 return 返回值
};

6. map() filter() reduce() 返回值分别是什么?

1)map() 方法:创建一个新数组,其结果是对原数组中的每个元素调用提供的函数后的返回值。
const numbers = [1, 2, 3];
const doubled = numbers.map(num => num * 2); 
// doubled 的结果为 [2, 4, 6]
2)filter() 方法:创建一个新数组,其中包含通过提供的函数实现的测试的所有元素。
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0); 
// evenNumbers 的结果为 [2, 4]
3)reduce() 方法:对数组中的每个元素执行一个提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0); 
// sum 的结果为 15

在这些方法中, reduce() 的返回结果通常与 map() 和 filter() 有所不同,因为它最终返回的是一个汇总的值,而不是一个新的数组。

7.阻止事件默认行为的方式有哪些?

1)在事件处理函数中返回 false :
<button onclick="handleClick(); return false;">点击我</button>
<script>
  function handleClick() {
    // 事件处理逻辑
  }
</script>
2)使用 preventDefault() 方法:
<button id="myButton">点击我</button>
<script>
  document.getElementById('myButton').addEventListener('click', function(event) {
    event.preventDefault();
    // 事件处理逻辑
  });
</script>
3)对于表单提交事件,如果使用 jQuery 库,可以这样阻止:
<form id="myForm">
  <!-- 表单元素 -->
</form>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
  $('#myForm').submit(function(event) {
    event.preventDefault();
    // 表单处理逻辑
  });
</script>

8.设置事件时,最后参数true和false,冒泡和捕获的触发顺序是怎样的?

在设置事件处理程序时, true 表示使用捕获阶段, false (默认值)表示使用冒泡阶段。

事件传播的顺序如下:

1)捕获阶段:

从最外层的祖先元素开始,向目标元素(触发事件的元素)传播。

2)目标阶段:

到达实际触发事件的目标元素。

3)冒泡阶段:

从目标元素开始,向其祖先元素传播。

如果将最后一个参数设置为 true (捕获阶段),事件会先从最外层的祖先元素触发,然后依次向内传播,直到到达目标元素。

如果将最后一个参数设置为 false (冒泡阶段),事件会先在目标元素触发,然后依次向其祖先元素传播。

9.逻辑或 和 逻辑与

  • “逻辑或”||:
    • 当两个操作数中至少有一个为真(即非零、非空、非 false 等)时,整个表达式为真。
    • 只有当两个操作数都为假时,表达式才为假。

例如: true || false 的结果为 true , false || false 的结果为 false 。

  • “逻辑与”&&:
    • 只有当两个操作数都为真时,整个表达式才为真。
    • 如果其中至少有一个为假,表达式就为假。

例如: true && true 的结果为 true , true && false 的结果为 false 。

10.在 JavaScript 中,可以使用哪几种方法来判断数据类型是否相等?

1)typeof 操作符:可以返回一个表示数据类型的字符串。

但对于某些类型(如 null 和 object 类型的数组、对象)的判断不够精确。

2)instanceof 操作符:用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

但对于基本数据类型(如 Number 、 String 、 Boolean 等)不适用。

3)Object.prototype.toString.call() 方法:这是一种比较准确和全面的判断数据类型的方法。

它会返回一个形如 [object Type] 的字符串,其中 Type 表示具体的数据类型。

例如:
console.log(Object.prototype.toString.call(5)); // "[object Number]"
console.log(Object.prototype.toString.call('hello')); // "[object String]"
console.log(Object.prototype.toString.call(true)); // "[object Boolean]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call([])); // "[object Array]"

11.对微任务宏任务的理解

在 JavaScript 中,任务分为微任务(microtask)和宏任务(macrotask)。

1)宏任务
  • 定义:宏任务是由宿主环境(如浏览器、Node.js)发起的任务,通常是一些较大的、耗时较长的操作,例如:
    • setTimeout、setInterval定时器回调。
    • setImmediate(在 `Node.js 中)。
    • I/O 操作回调,如文件读取完成后的回调。
    • UI 渲染相关任务(在浏览器中)。
  • 执行过程:
    • 当一个宏任务执行完毕后,JavaScript 引擎会检查是否有微任务队列,如果有微任务,则会先执行微任务队列中的所有任务,然后再执行下一个宏任务。
    • 宏任务的执行会阻塞后续代码的执行,直到宏任务完成。
2)微任务
  • 定义:微任务是在当前任务执行结束后立即执行的任务,通常是一些较小的、需要尽快执行的操作,例如:
    • Promise的回调函数(then、catch、finally)。
    • MutationObserver的回调函数(用于监听 DOM 变化)。
    • process.nextTick(在 Node.js 中)。
  • 执行过程:
    • 当一个微任务被添加到微任务队列时,它会在当前任务执行结束后立即执行,而不会等待下一个宏任务的执行。
    • 微任务的执行会优先于宏任务,这意味着如果有多个微任务被添加到队列中,它们会按照添加的顺序依次执行,直到微任务队列为空。
3)执行顺序示例

以下是一个代码示例来说明微任务和宏任务的执行顺序:

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('promise1');
}).then(() => {
  console.log('promise2');
});

console.log('script end');

在这个例子中:

  • 首先,打印script start。
  • 然后遇到setTimeout,将其回调函数添加到宏任务队列中。
  • 接着遇到Promise.resolve().then(...),将其回调函数添加到微任务队列中。
  • 打印script end。
  • 当前脚本执行完毕,开始检查微任务队列,执行微任务队列中的任务,打印promise1和promise2。
  • 微任务队列为空,开始执行下一个宏任务,打印setTimeout。

综上所述,JavaScript 中的任务执行顺序是:

  • 先执行同步代码,
  • 然后执行微任务队列中的任务,
  • 最后执行宏任务队列中的任务。

了解微任务和宏任务的执行顺序对于处理异步操作和确保代码的正确执行顺序非常重要。

12.会造成js内存泄漏的是?

1)全局变量:

如果大量使用全局变量存储大型数据结构,并且这些变量在不再需要时没有被释放,会导致内存泄漏。

2)闭包:

如果闭包中引用了大量不再使用的变量或对象,但闭包一直存在并且引用未被释放,可能造成内存泄漏。

3)定时器未及时清除:

如果设置了定时器(如 setInterval 或 setTimeout ),但在不再需要时没有使用相应的方法(如 clearInterval 或 clearTimeout )来清除定时器,会导致相关的回调函数和资源一直占用内存。

4)事件监听未移除:

给元素添加事件监听后,如果在元素不再使用或相关操作结束时没有移除监听,会导致内存泄漏。

5)对 DOM 元素的引用未清除:

如果在 JavaScript 中保持了对已从 DOM 中移除的元素的引用,而这些引用未被释放,可能导致内存泄漏。

6)缓存未合理清理:

例如创建了一个大型的缓存对象来存储数据,但没有有效的策略来删除不再使用的数据,可能导致内存不断增长。

13.说说 var let const的区别

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

1)作用域
  • var:
    • 函数作用域:使用var声明的变量具有函数作用域。这意味着在函数内部声明的变量在函数外部是不可访问的,而在函数外部声明的变量在函数内部可以通过函数作用域链访问到。
    • 变量提升:使用var声明的变量会被提升到函数作用域的顶部。这意味着可以在变量声明之前使用变量,但是变量的值是undefined。
    • 重复声明:在同一个作用域内,可以使用var重复声明同一个变量,后面的声明会覆盖前面的声明。
  • let:
    • 块级作用域:使用let声明的变量具有块级作用域。块级作用域是由一对花括号{}包围的代码块,例如if语句、for循环、函数等。在块级作用域内声明的变量在块级作用域外部是不可访问的。
    • 变量提升:使用let声明的变量也会被提升,但是在变量声明之前使用变量会抛出ReferenceError错误。
    • 重复声明:在同一个块级作用域内,不能使用let重复声明同一个变量。
  • const:
  • 块级作用域:与let一样,使用const声明的变量也具有块级作用域。
  • 常量:使用const声明的变量是常量,一旦声明,其值不能被重新赋值。
  • 变量提升:与let类似,const声明的变量也会被提升,但是在变量声明之前使用变量会抛出ReferenceError错误。
  • 重复声明:在同一个块级作用域内,不能使用const重复声明同一个变量。
2)初始化
  • var:可以在声明时不进行初始化,此时变量的值为undefined。也可以在声明的同时进行初始化。
  • let:可以在声明时不进行初始化,此时变量的值为undefined。也可以在声明的同时进行初始化。
  • const:必须在声明时进行初始化,否则会抛出SyntaxError错误。
3)示例

以下是一个示例代码,展示了var、let和const的区别:

function example() {
  // var
  var varVariable = 1;
  if (true) {
    var varVariable = 2; // 可以重复声明
    console.log(varVariable); // 2
  }
  console.log(varVariable); // 2

  // let
  let letVariable = 1;
  if (true) {
    let letVariable = 2; // 不能重复声明,但在不同块级作用域内可以重新声明
    console.log(letVariable); // 2
  }
  console.log(letVariable); // 1

  // const
  const constVariable = 1;
  if (true) {
    // const constVariable = 2; // 会抛出错误,不能重复声明
    // constVariable = 2; // 会抛出错误,不能重新赋值
    console.log(constVariable); // 1
  }
  console.log(constVariable); // 1
}

example();
4)总结
  • 在大多数情况下,应该优先使用let和const来声明变量,因为它们具有更严格的作用域和更好的安全性。
  • 如果需要声明一个变量,并且在后续的代码中可能会重新赋值,应该使用let。
  • 如果需要声明一个常量,并且在后续的代码中不会重新赋值,应该使用const。
  • 尽量避免使用var,因为它的作用域和变量提升行为可能会导致一些难以调试的错误。

14.Promise 对象

1)Promise :

对象是 JavaScript 中用于处理异步操作的一种机制。

2)Promise 对象有三种状态:

pending (进行中)、 fulfilled (已成功)和 rejected (已失败)。

  • 创建一个 Promise 对象通常需要传入一个执行函数,该函数接受两个参数: resolve 和 reject 。
  • 当异步操作成功时,调用 resolve 函数并传递结果;当异步操作失败时,调用 reject 函数并传递错误原因。
  • 使用 then 方法来处理 Promise 对象成功的情况,
  • 使用 catch 方法来处理失败的情况,
  • 使用 finally 方法来定义无论 Promise 对象是成功还是失败都会执行的操作。
例如:
const myPromise = new Promise((resolve, reject) => {
  // 模拟异步操作
  setTimeout(() => {
    const randomNumber = Math.random();
    if (randomNumber > 0.5) {
      resolve('操作成功,随机数大于 0.5: ' + randomNumber);
    } else {
      reject('操作失败,随机数小于等于 0.5: ' + randomNumber);
    }
  }, 2000);
});

myPromise.then(result => {
  console.log(result);
}).catch(error => {
  console.error(error);
});

Promise 对象使得异步代码的编写和处理更加结构化和可控。

15.js 继承的多种实现方式

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

Parent.prototype.sayHello = function() {
  console.log('Hello from Parent');
}

function Child() {}

Child.prototype = new Parent();

let child = new Child();
child.sayHello(); 
2)借用构造函数继承(经典继承):
function Parent(name) {
  this.name = name;
}

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

let child = new Child('Child Name');
console.log(child.name); 
3)组合继承(原型链 + 借用构造函数):
function Parent(name) {
  this.name = name;
}

Parent.prototype.sayHello = function() {
  console.log('Hello from Parent');
}

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

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

let child = new Child('Child Name');
child.sayHello(); 
4) 原型式继承:
function createObject(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

let parent = {name: 'Parent'};
let child = createObject(parent);
5)寄生式继承:
function createChild(obj) {
  let clone = Object.create(obj);
  clone.sayHi = function() {
    console.log('Hi');
  };
  return clone;
}

let parent = {name: 'Parent'};
let child = createChild(parent);
6)寄生组合式继承:
function inheritPrototype(child, parent) {
  let prototype = Object.create(parent.prototype);
  prototype.constructor = child;
  child.prototype = prototype;
}

function Parent() {}
Parent.prototype.sayHello = function() {
  console.log('Hello from Parent');
}

function Child() {}

inheritPrototype(Child, Parent);

let child = new Child();
child.sayHello(); 

16.28.Date 对象

在 JavaScript 中, Date 对象用于处理日期和时间。

可以通过以下几种方式创建 Date 对象:

1)不传递参数,创建一个表示当前日期和时间的 Date 对象:
let now = new Date();
2)传递一个表示日期时间的字符串:
let specificDate = new Date("2024-08-04");
3)传递年、月、日、时、分、秒等参数:
let customDate = new Date(2024, 7, 4, 10, 30, 0); 
// 注意:月份从 0 开始计数,0 表示 1 月,7 表示 8 月
4)Date 对象提供了许多方法来获取和操作日期时间的各个部分,例如:
  • getFullYear() :获取年份。
  • getMonth() :获取月份(0 - 11)。
  • getDate() :获取日期(1 - 31)。
  • getHours() :获取小时(0 - 23)。
  • getMinutes() :获取分钟(0 - 59)。
  • getSeconds() :获取秒数(0 - 59)。

还可以使用 toLocaleString() 等方法将日期对象转换为特定格式的字符串进行输出。

17.空数组的 valueof 和 toString 值

  • 空数组的 valueOf() 方法返回的是数组本身,
  • 而 toString() 方法将空数组转换为空字符串 "" 。
以下是示例代码:

let emptyArray = [];
console.log(emptyArray.valueOf()); 
console.log(emptyArray.toString());

18.js阻止冒泡的方法是?

1)使用 event.stopPropagation() 方法:
function handleClick(event) {
  event.stopPropagation();
  // 其他处理逻辑
}
2)在 IE 浏览器中(某些旧版本),可以使用 event.cancelBubble = true :
function handleClick(event) {
  if (event && event.stopPropagation) {
    event.stopPropagation();
  } else {
    event.cancelBubble = true;
  }
  // 其他处理逻辑
}

在实际开发中,建议优先使用 event.stopPropagation() 方法,因为它具有更好的兼容性和规范性。

19.阻止表单自动提交的方法:

1)在表单的提交按钮上,如果是 type="submit" ,可以将其修改为 type="button" ,这样点击按钮时就不会自动提交表单。
2)在表单的提交事件处理函数中,如果条件不满足,使用 event.preventDefault() 方法来阻止默认行为,即表单的自动提交。
document.querySelector('form').addEventListener('submit', function(event) {
  // 检查条件,如果不满足则阻止提交
  if (/* 条件不满足 */) {
    event.preventDefault();
  }
});

20.js array不同方法的返回值?

  • pop() 方法:用于删除数组的最后一个元素,并返回被删除的元素。
  • shift() 方法:用于删除数组的第一个元素,并返回被删除的元素。
  • push() 、 unshift() 等方法:返回的是修改后的数组的新长度。
  • sort() 、 reverse() 等方法:是直接修改原数组,返回值为修改后的数组本身。
  • filter() 、map() 、 reduce() 等方法:返回的是一个新的数组。

21.在 JavaScript 中,一些常见的全局函数

  1. parseInt() :将字符串转换为整数。
  2. parseFloat() :将字符串转换为浮点数。
  3. isNaN() :判断一个值是否为非数字值(Not a Number)。
  4. encodeURI() 和 encodeURIComponent() :对 URI 进行编码。
  5. decodeURI() 和 decodeURIComponent() :对编码后的 URI 进行解码。
  6. eval() :将字符串作为 JavaScript 代码进行求值。

这些只是 JavaScript 全局函数的一部分,还有其他一些函数用于各种不同的功能。

22. 什么是深拷贝?什么是浅拷贝?

在 JavaScript 中,深拷贝和浅拷贝是两种复制对象的方式。

1)浅拷贝(Shallow Copy)
  • 定义:
    • 浅拷贝创建一个新的对象,这个对象有着原始对象属性值的一份精确拷贝。
    • 如果属性是基本类型,拷贝的就是基本类型的值;
    • 如果属性是引用类型,拷贝的就是内存地址,
    • 所以如果其中一个对象改变了这个地址中的内容,就会影响到另一个对象。
  • 示例:
  let obj1 = { a: 1, b: { c: 2 } };
  let obj2 = Object.assign({}, obj1);
  // 或者
  let obj2 = {...obj1 };

  obj1.a = 2;
  console.log(obj2.a); // 1(未受影响,因为基本类型值独立)
  obj1.b.c = 3;
  console.log(obj2.b.c); // 3(受影响,因为引用类型地址相同)
2)深拷贝(Deep Copy)
  • 定义:
    • 深拷贝会递归地复制原始对象及其所有嵌套对象,
    • 创建一个完全独立的副本,
    • 新副本与原始对象没有任何关联,
    • 修改其中一个不会影响另一个。
  • 方法:
    • JSON.parse(JSON.stringify()):
     let obj1 = { a: 1, b: { c: 2 } };
     let obj2 = JSON.parse(JSON.stringify(obj1));
    
     obj1.b.c = 3;
     console.log(obj2.b.c); // 2(未受影响,实现了深拷贝)
    
    【 注意事项】不支持函数、Symbol、循环引用等。
    • 递归实现:
     function deepCopy(obj) {
       if (typeof obj!== 'object' || obj === null) {
         return obj;
       }
    
       let newObj = Array.isArray(obj)? [] : {};
    
       for (let key in obj) {
         newObj[key] = deepCopy(obj[key]);
       }
    
       return newObj;
     }
    
     let obj1 = { a: 1, b: { c: 2 } };
     let obj2 = deepCopy(obj1);
    
     obj1.b.c = 3;
     console.log(obj2.b.c); // 2(未受影响,实现了深拷贝)
    
    • 使用库:如lodash的_.cloneDeep()方法可以方便地实现深拷贝。
    const _ = require('lodash');
    
    let obj1 = { a: 1, b: { c: 2 } };
    let obj2 = _.cloneDeep(obj1);
    
    obj1.b.c = 3;
    console.log(obj2.b.c); // 2(未受影响,实现了深拷贝)
    

23.深拷贝和浅拷贝在实际开发中的应用场景?

1)浅拷贝的应用场景
  • 快速创建对象副本进行一些简单的操作:
    • 当只需要对一个对象进行一些临时的修改,而不希望影响原始对象时,可以使用浅拷贝创建一个副本。
    • 例如,在一个函数中需要对传入的参数对象进行一些操作,但不希望改变原始对象,可以创建一个浅拷贝的对象进行操作。
    • 示例:
      function processObject(obj) {
        let copyObj = {...obj };
        // 对 copyObj 进行一些操作,不会影响原始的 obj
        copyObj.property = newValue;
        return copyObj;
      }
      
  • 对象合并:
    • 可以使用浅拷贝的方式(如Object.assign()或扩展运算符{...})将多个对象的属性合并到一个新的对象中。
    • 示例:
      let obj1 = { a: 1, b: 2 };
      let obj2 = { c: 3, d: 4 };
      let mergedObj = {...obj1,...obj2 };
      
2)深拷贝的应用场景
  • 复杂对象的复制:
    • 当对象结构比较复杂,包含嵌套的对象或数组时,为了确保完全独立的副本,需要使用深拷贝。
    • 例如,在一个数据处理的应用中,可能需要对一个包含多层嵌套对象的数据集进行操作,为了不影响原始数据,需要进行深拷贝。
    • 示例:
      let originalData = {
        name: 'John',
        details: {
          age: 30,
          hobbies: ['reading', 'coding']
        }
      };
      let copiedData = deepCopy(originalData);
      // 对 copiedData 进行操作,不会影响 originalData
      
  • 避免数据共享和副作用:
    • 在一些情况下,多个部分的代码可能会共享一个对象,如果其中一个部分对对象进行了修改,可能会导致意外的副作用。
    • 通过深拷贝可以确保每个部分都有自己独立的对象副本,避免这种副作用。
    • 例如,在一个多线程的应用中,每个线程可能需要自己独立的数据集,这时可以对原始数据集进行深拷贝,确保每个线程操作的是独立的副本。
  • 数据持久化和备份:
    • 当需要对数据进行持久化或备份时,深拷贝可以创建一个完全独立的副本,以便在需要时恢复原始数据。
    • 例如,在一个在线文档编辑应用中,可以在用户进行重要操作之前对文档数据进行深拷贝,以便在出现错误时可以恢复到之前的状态。
  • 传递参数给函数:
    • 如果一个函数需要对传入的对象进行深度修改,但不希望影响原始对象,可以使用深拷贝将对象复制一份后再在函数内部进行操作。
    • 示例:
      function modifyData(data) {
        let copiedData = deepCopy(data);
        // 对 copiedData 进行深度修改
        return copiedData;
      }
      

24.对Json.parse() 和Json.stringify()的理解

JSON.parse() 和 JSON.stringify() 是 JavaScript 中用于处理 JSON 数据的两个重要方法。

  • JSON.parse() 方法用于将一个有效的 JSON 字符串转换为 JavaScript 对象。
    • 例如,如果有一个 JSON 字符串 '{"name": "John", "age": 30}' ,通过 JSON.parse() 可以将其转换为一个 JavaScript 对象 {name: "John", age: 30} ,以便在代码中进行操作和使用。
  • JSON.stringify() 方法则相反,它用于将 JavaScript 对象转换为 JSON 字符串。
    • 比如,对于对象 {name: "John", age: 30} ,使用 JSON.stringify() 会得到字符串 '{"name": "John", "age": 30}' ,这在需要将数据发送到服务器或进行存储时非常有用。

【注意】

  • JSON.parse() 要求输入的字符串必须是符合 JSON 格式的,否则会抛出错误。
  • JSON.stringify() 对于某些不能被序列化为 JSON 的对象属性(如函数、 Symbol 等)会被忽略。

25.promise是同步还是异步的?Then方法呢?

1)promise本身是同步的
let oP = new Promise( (res, rej) => {
     	console.log(1);
});
console.log(2);

执行的结果先打印出1再打印出2,如果promise是异步的应该先打印出2,所以promise本身是同步。

2)Then异步
let oP = new Promise((res, rej) => {
      console.log(1);
      res(3)
    });
    oP.then((res) => {
      console.log(res);
    });
    console.log(2);

执行的结果1,2,3,因为then是异步的,所以先打印了2,最后再执行回调打印出3。

26.前端中的事件流:

前端事件流是指当用户与网页交互触发一个事件时,事件是如何从触发元素逐级向上冒泡(Bubble)到文档根节点,或者从触发元素向下传播(Capture)到最具体的子元素的过程。通常分为三个阶段:

1)捕获阶段(Event Capture):
  • 事件从目标元素开始,沿着DOM树的结构从上至下(window -> document -> 元素)依次触发相应的事件处理器函数。
  • 在这个阶段,如果存在对应的捕获处理程序,它会先被执行。
2)目标阶段(Event at Target):
  • 这是默认的行为,在这个阶段,事件首先到达目标元素,该元素上已注册的事件处理器会执行。
3)冒泡阶段(Event Bubble):
  • 当事件从目标节点传递到其父节点,然后再到父的父节点,直至达到window对象,这被称为冒泡过程。
  • 在这个阶段,所有在冒泡路径上的元素都有机会处理事件。

开发者可以使用addEventListener等方法注册事件监听器,
并通过event.stopPropagation()或event.stopImmediatePropagation()来阻止事件继续向上传播或者停止当前层的冒泡。

27.在js中,如何去判断某个变量是什么类型的?

1)typeof 操作符
  • typeof 是判断基本数据类型的最常见方法。它返回一个表示变量类型的字符串。
  • 常用的返回值包括:
    • "undefined":变量未定义
    • "boolean":布尔值
    • "number":数字
    • "string":字符串
    • "object":对象或 null
    • "function":函数
  • 示例:
let a = 5;
console.log(typeof a); // "number" 
let b = "Hello"; 
console.log(typeof b); // "string" 
let c = null; 
console.log(typeof c); // "object" 
let d = {}; 
console.log(typeof d); // "object" 
let e = function() {};
console.log(typeof e); // "function"
2)instanceof 运算符
  • instanceof 可以用来判断一个对象是否是某个构造函数的实例。适用于对象的判断。
  • 示例:
let arr = [1, 2, 3]; 
console.log(arr instanceof Array); // true 
let date = new Date();
console.log(date instanceof Date); // true
3)Object.prototype.toString.call()
  • 这是一个更为准确的方法,可以判断各种数据类型,尤其是在处理数组和null时非常有效。
  • 示例:
console.log(Object.prototype.toString.call([]));//"[object  Array]" 
console.log(Object.prototype.toString.call({}));// "[object Object]" 
console.log(Object.prototype.toString.call(null)); // "[object Null]" 
console.log(Object.prototype.toString.call(42)); // "[object Number]" 
console.log(Object.prototype.toString.call("text")); // "[object String]"
4)使用 constructor 属性
  • constructor 属性也可以帮助判断对象的类型,但要注意,对于一些基本类型(如string和number),结果可能不如 typeof 清晰。
  • 示例:
let str = "str"; console.log(str.constructor === String); // true 
let num = 10; console.log(num.constructor === Number); // true
5)jQuery 的 $.type()
  • 如果使用 jQuery,可以利用其提供的类型检测函数 $.type()。
  • 示例:
console.log($.type("hello")); //"string" 
console.log($.type(123)); // "number" 
console.log($.type([])); // "array"

【总结】
在 JavaScript 中,最常用的是 typeof 结合 instanceof 和Object.prototype.toString.call()。
这些方法可以共同使用,以便更全面和精确地判断变量类型。

28.在js中,数组排序如何实现 (具体到代码逻辑,不能只说函数方法)?

在 JavaScript 中,数组排序主要通过 Array.prototype.sort() 方法实现。该方法可以按照字母顺序对数组元素进行排序,但在处理数字时需要注意,因为默认的排序方式是将元素转换为字符串进行比较。

以下是关于数组排序的详细说明:

1)sort() 方法:会改变原数组,并返回一个排序后的数组。
  • 默认情况下,它会按照每个元素的 UTF-16 值进行升序排序,
  • 例如:
let fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.sort(); // 结果为 ["Apple", "Banana", "Mango", "Orange"]
2)数字排序
  • 对于数字排序,默认的 sort() 方法并不会按数字大小进行排序,而是按字符串顺序进行排序。
  • 例如:
let numbers = [4, 2, 5, 1, 3];
numbers.sort(); // 结果为 [1, 2, 3, 4, 5],这是字符串排序而非数字排序。
  • 因此,要进行正确的数字排序,需要传递一个比较函数。
  • 比较函数接收两个参数,返回值决定了这两个元素的顺序:
    • 如果返回小于 0,a 会排在 b 之前。
    • 如果返回大于 0,b 会排在 a 之前。
    • 如果返回 0,a 和 b 的相对位置不变。
  • 示例:
let numbers = [4, 2, 5, 1, 3];
numbers.sort((a, b) => a - b); // 结果为 [1, 2, 3, 4, 5]
3)自定义排序
  • 如果需要按照对象的属性进行排序,可以在比较函数中定义逻辑。
  • 例如,如果有一个对象数组,需要按照某个属性进行排序:
let users = [ { name: "John", age: 25 }, { name: "Jane", age: 20 }, { name: "Mike", age: 30 } ]; 
users.sort((a, b) => a.age - b.age); // 根据年龄升序排序 
// 结果为 [{ name: "Jane", age: 20 }, { name: "John", age: 25 }, { name: "Mike", age: 30 }]
4)逆序排序
  • 如果想要实现降序排序,可以将比较函数的返回值反转:
let numbers = [4, 2, 5, 1, 3];
 numbers.sort((a, b) => b - a); // 结果为 [5, 4, 3, 2, 1]

总结

  • Array.prototype.sort() 方法是 JavaScript 中强大的数组排序工具。
  • 通过传递比较函数,可以灵活地按照不同的规则对数组进行排序。
  • 注意在处理数字时一定要使用比较函数,以确保排序结果符合预期。

29.在js中,去重如何实现,具体到逻辑实现:

1)使用 Set
  • Set 是一种集合类型,它只允许存储唯一的值。
  • 可以利用这一特性实现数组去重。
  • 实现逻辑:
    • 将原数组传递给 Set 构造函数。
    • 将 Set 转换回数组。
  • 示例代码:
const arr = [1, 2, 2, 3, 4, 4, 5]; 
const uniqueArr = Array.from(new Set(arr));
 console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]
2)使用 filter 和 indexOf
  • 可以利用数组的 filter 方法,根据元素的首次出现位置来判断是否重复。
  • 实现逻辑:
    • 使用 filter 遍历数组。
    • 在回调函数中使用 indexOf 检查当前元素第一次出现的位置是否与当前索引相同。
    • 示例代码:
const arr = [1, 2, 2, 3, 4, 4, 5]; 
const uniqueArr = arr.filter((item, index) => arr.indexOf(item) === index); 
console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]
3) 使用 reduce
  • 可以使用 reduce 方法简化处理,构建一个新的数组并在过程中去重。
  • 实现逻辑:
    • 使用 reduce 方法遍历原数组。
    • 在累积器中判断当前元素是否已存在,如果不存在,则加入。
    • 示例代码:
const arr = [1, 2, 2, 3, 4, 4, 5]; 
const uniqueArr = arr.reduce((acc, item) => { 
if (!acc.includes(item)) { acc.push(item); } 
return acc; }, []);
 console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]
4) 使用对象的属性
  • 可以利用对象的键唯一性来实现去重。
  • 实现逻辑:
    • 创建一个空对象。
    • 遍历原数组,将元素作为对象的键,只有当键不存在时才将其添加到结果数组中。
    • 示例代码:
const arr = [1, 2, 2, 3, 4, 4, 5];
const uniqueArr = []; 
const seen = {}; 
arr.forEach(item => { if (!seen[item]) { 
uniqueArr.push(item); 
seen[item] = true; // 将这个元素标记为已见过 } });
 console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]
5)使用 Map
  • Map 结构同样允许确保唯一性,可以用于去重。
  • 实现逻辑:
    • 创建一个空 Map。
    • 遍历原数组,将每个元素作为 Map 的键。
    • 示例代码:
const arr = [1, 2, 2, 3, 4, 4, 5]; 
const uniqueArr = [...new Map(arr.map(item => [item, item])).values()]; 
console.log(uniqueArr); // 输出: [1, 2, 3, 4, 5]

30.在js中,map()和forEach()的区别:

1)返回值
  • map(): 返回一个新数组,这个新数组由调用提供的函数处理后的每个元素组成。
    • 即使原始数组保持不变,map() 方法会生成一个新的数组。
    • 示例:
const numbers = [1, 2, 3]; 
const doubled = numbers.map(num => num * 2);
 	console.log(doubled); // 输出: [2, 4, 6]
  • forEach(): 返回 undefined。
    • 该方法在对原数组的每个元素执行回调函数,但不会生成新数组,主要用于遍历。
    • 示例:
const numbers = [1, 2, 3]; 
numbers.forEach(num => console.log(num * 2)); // 输出: 2 4 6
2) 用途
  • map():
    • 用于对原数组中的每个元素进行转换,并生成一个新的数组。
    • 适用于需要获取经过转换后的元素集合的情况。
    • 例如,将数组中的每个数字翻倍、格式化字符串等。
  • forEach():
    • 用于遍历数组中的每个元素,不需要返回新数组的情况。
    • 适合执行一些副作用操作,比如打印元素或更新外部状态,而不关心结果。
3)链式调用
  • map(): 由于返回的是新数组,可以与其他数组方法链式调用,像 filter()、reduce() 等。
    示例:
const numbers = [1, 2, 3, 4]; 
const result = numbers.map(num => num * 2).filter(num => num > 5); 
console.log(result); // 输出: [6, 8]
  • forEach(): 由于返回 undefined,不能与其他数组方法链式调用。
4)性能方面
  • 虽然在大多数情况下性能差异不明显,但 map() 会创建一个新的数组,可能会占用更高的内存;
  • 而 forEach() 仅用于遍历,不会造成额外的内存开销。

31.数组很大的时候,快速查找某一目标项,如何解决这个问题:

在 JavaScript 中处理大数组时,若需要快速查找某一目标项,可以考虑以下几种方法:

1)使用哈希表
  • 如果你需要频繁查找,可以使用对象或 Map 作为哈希表来存储数组元素及其索引。
  • 查找时间复杂度可降低到 O(1)。
  • 示例代码:
function createHashTable(arr) { 
const hashTable = new Map(); // 使用 Map 存储 
arr.forEach((value, index) => { hashTable.set(value, index); // 将元素及其索引存入哈希表 }); 
return hashTable; 
}  
  • 示例
const arr = [10, 20, 30, 40, 50]; 
const hashTable = createHashTable(arr); 
const target = 30; 
const index = hashTable.get(target); // 快速查找目标元素的索引 
console.log(index); // 输出:2
2)二分查找(适用于有序数组)
  • 如果数组是已排序的,可以使用二分查找。
  • 该算法每次将搜索范围缩小一半,查找效率高,时间复杂度为 O(log n)。
  • 示例代码:
function binarySearch(arr, target) { 
let left = 0; 
let right = arr.length - 1; 
while (left <= right) { 
const mid = Math.floor((left + right) / 2); 
if (arr[mid] === target) { return mid; // 找到目标元素 } 
if (arr[mid] < target) { left = mid + 1; // 在右侧查找 } 
else { right = mid - 1; // 在左侧查找 } 
} 
return -1; // 未找到目标元素 
} 

示例

const sortedArr = [10, 20, 30, 40, 50]; 
const target = 30; 
const index = binarySearch(sortedArr, target); 
console.log(index); // 输出:2
3)使用线性查找
  • 对于无序数组,线性查找是一种简单的方法。
  • 尽管时间复杂度为 O(n),但在实际应用中,如果只需要查找一次,或者数组较小,线性查找也可以接受。
  • 示例代码:
function linearSearch(arr, target) { 
for (let i = 0; i < arr.length; i++) { 
if (arr[i] === target) { return i; // 找到目标元素 } 
} return -1; // 未找到目标元素 
} 

示例

const arr = [50, 20, 10, 30, 40]; 	
const target = 30; 
const index = linearSearch(arr, target);
 console.log(index); // 输出:3
4) 使用 findIndex 方法
  • JavaScript 数组的内置方法 findIndex 也可以用来查找元素,
  • 特别是在需要查找对象数组中的特定属性时非常方便。
  • 示例代码:
const arr = [10, 20, 30, 40, 50]; 
const target = 30; 
const index = arr.findIndex(item => item === target); // 查找目标元素的索引 
console.log(index); // 输出:2

总结

  • 如果需要频繁查找和大数据量,尽量使用哈希表(Map)。
  • 当数组是有序时,使用二分查找会更高效。
  • 对于一次或少量查找,可以直接使用线性查找或 findIndex 方法。
  • 选择合适的方法将取决于你的具体需求和数组的特性。

31.ES6新特性有哪些?

1)let 和 const 声明
  • let:
    • 具有块级作用域,解决了var声明变量存在的作用域问题。在{}内声明的let变量只在该块内有效。
    • 不存在变量提升,在声明之前使用会报错。
    • 不允许重复声明。
      例如:
{
  let x = 1;
  console.log(x); // 1
}
console.log(x); // ReferenceError: x is not defined
  • const:
    • 声明常量,一旦赋值就不能再被重新赋值。
    • 同样具有块级作用域,不存在变量提升,不允许重复声明。
      例如:
const PI = 3.14;
// PI = 3.15; // TypeError: Assignment to constant variable.
2)模板字符串

使用反引号(`)包裹字符串,可以在字符串中嵌入变量和表达式,并且支持多行字符串。

例如:

const name = "Alice";
const greeting = `Hello, ${name}!`;
console.log(greeting); // Hello, Alice!

const multilineText = `This is a
multiline
string.`;
console.log(multilineText);
3)箭头函数
  • 简洁的语法:
    • 可以使用更简洁的语法定义函数,特别是在只有一个表达式的情况下。
    • 例如:(param) => expression。

例如:

const add = (a, b) => a + b;
console.log(add(2, 3)); // 5
  • 词法作用域:
    • 箭头函数不会创建自己的this、arguments、super或new.target。它继承了外层函数的这些值。

例如:

function Person() {
  this.age = 30;
  setTimeout(() => {
    console.log(this.age); // 30
  }, 1000);
}
new Person();
4)解构赋值
  • 数组解构:
    • 可以从数组中提取值并赋值给变量。

例如:

const [a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2
  • 对象解构:
    • 可以从对象中提取属性并赋值给变量。

例如:

const { name, age } = { name: "Bob", age: 25 };
console.log(name); // Bob
console.log(age); // 25
5)默认参数值
  • 函数参数可以设置默认值,当函数调用时没有传递该参数时,将使用默认值。

例如:

function greet(name = "World") {
  console.log(`Hello, ${name}!`);
}
greet(); // Hello, World!
greet("Alice"); // Hello, Alice!
6)展开运算符(Spread Operator)
  • 用于数组:
    • 可以将一个数组展开为另一个数组的元素。

例如:

const arr1 = [1, 2, 3];
const arr2 = [4, 5,...arr1];
console.log(arr2); // [4, 5, 1, 2, 3]
  • 用于对象:
    • 可以将一个对象展开为另一个对象的属性。

例如:

const obj1 = { a: 1, b: 2 };
const obj2 = {...obj1, c: 3 };
console.log(obj2); // { a: 1, b: 2, c: 3 }
7)剩余参数(Rest Parameters)

函数参数可以使用剩余参数语法来表示不确定数量的参数,并将它们收集到一个数组中。

例如:

function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // 6
8)Promise

用于处理异步操作,提供了一种更优雅的方式来处理异步代码的回调地狱问题。

例如:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Success!");
  }, 1000);
});

promise.then((result) => {
  console.log(result); // Success!
}).catch((error) => {
  console.log(error);
});
9)Class(类)

提供了一种更接近传统面向对象编程的语法来定义类和处理继承。

例如:

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name);
  }
  speak() {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog("Fido");
dog.speak(); // Fido barks.
10)模块(Module)

使用import和export关键字来实现模块的导入和导出,更好地组织和管理代码。

例如:

// module1.js
export const message = "Hello from module1!";

// module2.js
import { message } from './module1';
console.log(message); // Hello from module1!

32.xmlhttpRequest的解释

XMLHttpRequest 是 JavaScript 中用于在客户端和服务器之间进行异步数据传输的对象。

它允许网页向服务器发送请求,并在不重新加载整个页面的情况下处理服务器的响应。这使得网页能够实现动态更新内容,提供更流畅和交互性更强的用户体验。

通过 XMLHttpRequest ,可以使用 open()方法设置请求的方法(如 GET 、 POST 等)和 URL ,使用 send() 方法发送请求,并通过监听 onreadystatechange 事件来获取服务器响应的状态和数据。

随着现代 Web 开发的发展,出现了更新的技术如 fetch API ,但 XMLHttpRequest 在一些旧的项目和特定场景中仍然被使用。

33.防抖和节流的应用场景?

防抖(debounce)和节流(throttle)是在前端开发中常用的优化技术,它们有不同的应用场景。

  • 防抖(debounce):
    • 在n秒后在执行该事件。
    • 如果在n秒内触发了很多次,那么重新计时。
    • 一般应用在搜索、input邮箱检索等。
  • 节流(throttle):
    • 在n秒内只执行一次该事件。
    • 若在n秒内重复触发,那么只执行一次。
    • 一般应用在滑动页面等。
1)防抖的应用场景
  • 输入框实时搜索:
    • 当用户在输入框中输入内容时,可能会触发实时搜索功能。
    • 如果每次输入都发送请求,会导致过多的网络请求和性能问题。
    • 使用防抖可以在用户停止输入一段时间后才触发搜索请求,减少不必要的请求。
    • 例如,在一个电商网站的搜索框中,当用户输入商品名称时,使用防抖技术延迟发送搜索请求,直到用户停止输入一段时间(如 500 毫秒)后才执行搜索。
  • 窗口调整大小:
    • 当用户调整浏览器窗口大小时,可能会触发一些需要重新计算布局或重新绘制页面的操作。
    • 如果每次窗口大小变化都立即执行这些操作,会导致性能问题。
    • 使用防抖可以在用户停止调整窗口大小一段时间后才执行这些操作,减少不必要的计算和绘制。
    • 例如,当窗口大小变化时,使用防抖技术延迟执行重新计算布局的函数,直到用户停止调整窗口大小一段时间(如 200 毫秒)后才执行。
  • 按钮点击事件:
    • 在某些情况下,用户可能会快速点击按钮多次,导致重复的操作。
    • 使用防抖可以确保在用户最后一次点击按钮一段时间后才执行相应的操作,避免重复执行。
    • 例如,在一个提交表单的按钮上,使用防抖技术延迟执行提交表单的函数,直到用户停止点击按钮一段时间(如 300 毫秒)后才提交表单。
2)节流的应用场景
  • 滚动事件:
    • 当用户滚动页面时,可能会触发一些需要实时更新的操作,如加载更多内容、计算滚动位置等。
    • 如果每次滚动都触发这些操作,会导致性能问题。
    • 使用节流可以限制这些操作的触发频率,例如每隔一定时间(如 200 毫秒)才执行一次。
    • 例如,在一个无限滚动的页面中,当用户滚动到底部时,使用节流技术每隔一段时间(如 500 毫秒)才加载更多内容,避免频繁的网络请求。
  • 鼠标移动事件:
    • 在某些应用中,需要实时跟踪鼠标的位置,但如果每次鼠标移动都触发相应的操作,会导致性能问题。
    • 使用节流可以限制鼠标移动事件的触发频率,例如每隔一定时间(如 50 毫秒)才执行一次跟踪鼠标位置的操作。
    • 例如,在一个绘图应用中,使用节流技术每隔一段时间(如 100 毫秒)才更新鼠标位置,减少不必要的计算和绘制。
  • 动画效果:
    • 在某些动画效果中,需要频繁地更新元素的位置或属性。
    • 如果每次更新都立即执行,会导致性能问题。
    • 使用节流可以限制动画效果的更新频率,例如每隔一定时间(如 16 毫秒,对应 60 帧每秒的动画帧率)才执行一次更新操作。
    • 例如,在一个平滑滚动的动画中,使用节流技术每隔一段时间(如 16 毫秒)才更新元素的位置,实现流畅的动画效果。

34. 在开发过程中,原型链的应用场景?

1)继承
  • 对象之间的继承:
    • 通过原型链可以实现对象之间的继承,让一个对象继承另一个对象的属性和方法。
    • 例如,可以创建一个父对象,然后通过原型链让子对象继承父对象的方法和属性,避免重复编写代码。
      function Parent() {
        this.parentProperty = 'parent value';
      }
    
      Parent.prototype.parentMethod = function() {
        console.log('Parent method');
      };
    
      function Child() {
        this.childProperty = 'child value';
      }
    
      Child.prototype = Object.create(Parent.prototype);
      Child.prototype.constructor = Child;
    
      var child = new Child();
      console.log(child.parentProperty); // 可以访问父对象的属性
      child.parentMethod(); // 可以调用父对象的方法
    
  • 类之间的继承:
    • 在使用类的语法时,也可以通过原型链实现类之间的继承。
    • 例如,在 ES6 中,可以使用extends关键字来实现类的继承,底层也是通过原型链来实现的。
     class Parent {
       constructor() {
         this.parentProperty = 'parent value';
       }
    
       parentMethod() {
         console.log('Parent method');
       }
     }
    
     class Child extends Parent {
       constructor() {
         super();
         this.childProperty = 'child value';
       }
     }
    
     const child = new Child();
     console.log(child.parentProperty); // 可以访问父类的属性
     child.parentMethod(); // 可以调用父类的方法
    
2)方法共享
  • 多个对象共享方法:
    • 通过将方法定义在原型上,可以让多个对象共享同一个方法,减少内存占用。
    • 例如,创建多个对象时,如果这些对象都需要使用同一个方法,可以将该方法定义在它们的原型上,而不是在每个对象中单独定义。
      function Person() {
        this.name = 'John';
      }
    
      Person.prototype.sayHello = function() {
        console.log(`Hello, my name is ${this.name}`);
      };
    
      const person1 = new Person();
      const person2 = new Person();
    
      person1.sayHello(); // 输出:Hello, my name is John
      person2.sayHello(); // 输出:Hello, my name is John
    
  • 插件和库的扩展:
    • 在开发插件或库时,可以利用原型链来扩展现有对象的功能。
    • 例如,为数组对象添加一个自定义的方法,可以通过在Array.prototype上定义新的方法来实现,这样所有的数组实例都可以使用这个方法。
      Array.prototype.myCustomMethod = function() {
        console.log('This is a custom method for arrays');
      };
    
      const arr = [1, 2, 3];
      arr.myCustomMethod(); // 输出:This is a custom method for arrays
    
3)对象的动态扩展
  • 运行时添加属性和方法:
    • 可以在运行时通过原型链为对象添加新的属性和方法,实现动态扩展。
    • 例如,根据特定的条件在运行时为对象添加一些额外的功能。
      function Animal() {
        this.species = 'unknown';
      }
    
      const dog = new Animal();
      dog.bark = function() {
        console.log('Woof!');
      };
    
      dog.bark(); // 输出:Woof!
    
  • 模拟多态性:
    • 通过在原型链上动态添加方法,可以实现类似多态性的效果。
    • 不同的对象可以根据需要在运行时拥有不同的行为。
      function Shape() {
        this.draw = function() {
          console.log('Drawing a generic shape');
        };
      }
    
      function Circle() {
        this.radius = 5;
      }
    
      Circle.prototype = Object.create(Shape.prototype);
      Circle.prototype.constructor = Circle;
    
      Circle.prototype.draw = function() {
        console.log(`Drawing a circle with radius ${this.radius}`);
      };
    
      const shape = new Shape();
      const circle = new Circle();
    
      shape.draw(); // 输出:Drawing a generic shape
      circle.draw(); // 输出:Drawing a circle with radius 5
    

35.箭头函数的作用

1)简洁的语法
  • 更简洁的函数定义:
    • 箭头函数提供了一种更简洁的方式来定义函数,特别是对于只有一个表达式的函数。
    • 例如,传统函数定义:function add(a, b) { return a + b; },使用箭头函数可以写成:const add = (a, b) => a + b;。
  • 省略函数体的花括号和return关键字:
    • 如果箭头函数的函数体只有一个表达式,那么可以省略花括号和return关键字,直接返回该表达式的结果。
    • 例如:const square = x => x * x;,这个箭头函数接受一个参数x,并返回x的平方。
2)词法作用域和this的绑定
  • 词法作用域:
    • 箭头函数不会创建自己的this、arguments、super或new.target值。相反,它使用封闭词法作用域中的这些值。
    • 这意味着在箭头函数内部,this的值取决于外部环境,而不是像传统函数那样在调用时确定。
  • 解决this指向问题:
    • 在 JavaScript 中,this的指向在不同的上下文中可能会发生变化,这可能会导致一些困惑和错误。箭头函数可以帮助解决这个问题,因为它的this指向是在定义时确定的,而不是在调用时确定。
    • 例如,在事件处理程序中,传统函数的this可能会指向触发事件的元素,而箭头函数的this将保持与外部作用域相同的值。
     const obj = {
       name: 'John',
       sayHello: function() {
         setTimeout(() => {
           console.log(`Hello, ${this.name}`);
         }, 1000);
       }
     };
    
     obj.sayHello(); // 输出:Hello, John
    
3)作为回调函数
  • 简洁的回调函数定义:
    • 箭头函数非常适合作为回调函数,特别是在数组方法(如map、filter、reduce等)和事件处理程序中。
    • 例如:const numbers = [1, 2, 3, 4, 5]; const doubledNumbers = numbers.map(num => num * 2);,这个代码使用箭头函数作为map方法的回调函数,将数组中的每个数字加倍。
  • 避免this指向问题:
    • 当在回调函数中需要访问外部作用域的this时,箭头函数可以避免this指向错误。
    • 例如,在 React 组件中,使用箭头函数作为事件处理程序可以确保this指向组件实例。
      class MyComponent extends React.Component {
        handleClick() {
          console.log(this);
        }
    
        render() {
          return <button onClick={() => this.handleClick()}>Click me</button>;
        }
      }
    
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐