js

js

let const var 的区别

letconstvar 都是 JavaScript 中用于声明变量的关键字,但它们在作用域、变量提升和可变性方面存在一些关键区别。

1. 作用域

  • var
    • var 声明的变量具有函数作用域。这意味着它们在声明它们的函数内部都是可访问的,如果在任何函数外部声明,则它们是全局可访问的。
    • var 没有块级作用域。这意味着在 if 语句、for 循环或任何其他块内部声明的变量在该块外部也是可访问的。
  • letconst
    • letconst 声明的变量具有块级作用域。这意味着它们只能在声明它们的块内部访问。
    • 块可以是任何用花括号 {} 包围的代码段,例如 if 语句、for 循环或函数。

2. 变量提升

  • var
    • var 声明的变量会被提升到其作用域的顶部。这意味着可以在声明变量之前使用它,但其值为 undefined
  • letconst
    • letconst 声明的变量不会被提升。如果在声明变量之前使用它们,会导致 ReferenceError。这称为“暂时性死区”。

3. 可变性

  • varlet
    • varlet 声明的变量是可变的,这意味着可以在声明后更改它们的值。
  • const
    • const 声明的变量是不可变的,这意味着一旦分配了值,就不能更改它。
    • 请注意,这并不意味着 const 声明的对象或数组是完全不可变的。可以修改 const 声明的对象或数组的属性或元素,但不能重新分配变量本身。

总结

特性 var let const
作用域 函数作用域 块级作用域 块级作用域
变量提升
可变性 可变 可变 不可变

使用建议

  • 在 ES6 及更高版本中,建议使用 letconst 而不是 var,以避免变量提升和作用域问题。
  • 如果变量的值在声明后不会更改,则应使用 const
  • 如果变量的值可能会更改,则应使用 let

js 中数组遍历的方式有几种

JavaScript 中有多种遍历数组的方式,每种方式都有其特定的用途和特点。以下是一些常见的数组遍历方法:

1. for 循环

  • 这是最传统的数组遍历方式。
  • 可以通过索引访问数组中的每个元素。
  • 适用于需要精确控制遍历过程的情况。
1
2
3
4
const arr = [1, 2, 3, 4, 5];
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}

2. forEach() 方法

  • forEach() 方法为数组中的每个元素执行一次提供的函数。
  • 它没有返回值,主要用于执行副作用(例如,打印元素或修改外部变量)。
  • 无法在 forEach() 循环中提前终止。
1
2
3
4
const arr = [1, 2, 3, 4, 5];
arr.forEach(element => {
  console.log(element);
});

3. map() 方法

  • map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
  • 它返回一个新数组,不会修改原始数组。
  • 适用于需要转换数组元素的情况。
1
2
3
const arr = [1, 2, 3, 4, 5];
const squaredArr = arr.map(element => element * element);
console.log(squaredArr); // [1, 4, 9, 16, 25]

4. filter() 方法

  • filter() 方法创建一个新数组,其中包含通过所提供函数实现的测试的所有元素。
  • 它返回一个新数组,不会修改原始数组。
  • 适用于需要筛选数组元素的情况。
1
2
3
const arr = [1, 2, 3, 4, 5];
const evenArr = arr.filter(element => element % 2 === 0);
console.log(evenArr); // [2, 4]

5. reduce() 方法

  • reduce() 方法对数组中的每个元素执行一个 reducer 函数(由您提供),从而将其简化为单个输出值。
  • 它接收一个累加器和一个当前值,并返回累加器的下一个值。
  • 适用于需要将数组元素聚合为一个值的情况。
1
2
3
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(sum); // 15

6. for...of 循环

  • for...of 循环可以遍历可迭代对象(包括数组)。
  • 它可以直接访问数组元素,而无需使用索引。
  • 适用于需要简单遍历数组元素的情况。
1
2
3
4
const arr = [1, 2, 3, 4, 5];
for (const element of arr) {
  console.log(element);
}

7. for...in 循环

  • for...in 循环可以遍历对象的可枚举属性。
  • 虽然它也可以用于遍历数组,但不建议这样做,因为它会遍历数组的所有可枚举属性,包括非索引属性。
  • 通常用于遍历对象。
1
2
3
4
const arr = [1, 2, 3, 4, 5];
for (const index in arr) {
  console.log(arr[index]);
}

总结

  • 如果需要精确控制遍历过程,可以使用 for 循环。
  • 如果需要执行副作用,可以使用 forEach() 方法。
  • 如果需要转换数组元素,可以使用 map() 方法。
  • 如果需要筛选数组元素,可以使用 filter() 方法。
  • 如果需要将数组元素聚合为一个值,可以使用 reduce() 方法。
  • 如果需要简单遍历数组元素,可以使用 for...of 循环。
  • 不建议使用 for...in 循环遍历数组。

判断js数据类型的方法

在 JavaScript 中,判断数据类型是开发过程中常见的需求。以下是一些常用的方法:

1. typeof 操作符

  • typeof 是一种简单的方法,用于返回一个表示操作数类型的字符串。
  • 它可以识别基本数据类型(除了 null),以及 function
  • 对于 objectarraynull 会返回 object
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typeof undefined; // "undefined"
typeof true; // "boolean"
typeof 10; // "number"
typeof "hello"; // "string"
typeof Symbol(); // "symbol"
typeof 10n; // "bigint"
typeof function() {}; // "function"
typeof {}; // "object"
typeof []; // "object"
typeof null; // "object"

2. instanceof 操作符

  • instanceof 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
  • 主要用于判断对象类型,例如 ArrayDateRegExp
  • 不能用于判断基本数据类型。
1
2
3
4
5
6
[] instanceof Array; // true
new Date() instanceof Date; // true
new RegExp() instanceof RegExp; // true
({}) instanceof Object; // true
10 instanceof Number; // false
"hello" instanceof String; // false

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

  • Object.prototype.toString.call() 是一种更可靠的方法,可以准确判断所有数据类型。
  • 它返回一个形如 "[object Type]" 的字符串,其中 Type 是对象的类型。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(10); // "[object Number]"
Object.prototype.toString.call("hello"); // "[object String]"
Object.prototype.toString.call(Symbol()); // "[object Symbol]"
Object.prototype.toString.call(10n); // "[object BigInt]"
Object.prototype.toString.call(function() {}); // "[object Function]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(null); // "[object Null]"

4. Array.isArray() 方法

  • Array.isArray() 是专门用于判断一个值是否为数组的方法。
  • 相比 instanceofObject.prototype.toString.call(),它更简洁和高效。
1
2
Array.isArray([]); // true
Array.isArray({}); // false

总结

  • typeof 适用于基本数据类型和 function 的判断。
  • instanceof 适用于对象类型的判断。
  • Object.prototype.toString.call() 适用于所有数据类型的准确判断。
  • Array.isArray() 适用于数组类型的判断。

在实际开发中,应根据具体情况选择合适的判断方法。

js中的this绑定问题

在 JavaScript 中,this 关键字的行为可能令人困惑,因为它不是静态绑定的,而是动态确定的。理解 this 的绑定规则对于编写健壮的 JavaScript 代码至关重要。

this 的绑定规则

  1. 默认绑定(Default Binding)

    • 在非严格模式下,如果 this 没有被其他规则显式绑定,它会默认绑定到全局对象(浏览器中是 window,Node.js 中是 global)。
    • 在严格模式下,this 会绑定到 undefined
    1
    2
    3
    4
    
    function foo() {
      console.log(this);
    }
    foo(); // 非严格模式下:window,严格模式下:undefined
    
  2. 隐式绑定(Implicit Binding)

    • 当函数作为对象的方法调用时,this 会隐式绑定到该对象。
    1
    2
    3
    4
    5
    6
    7
    
    const obj = {
      name: 'Alice',
      sayName: function() {
        console.log(this.name);
      }
    };
    obj.sayName(); // Alice
    
  3. 显式绑定(Explicit Binding)

    • 可以使用 call()apply()bind() 方法显式地指定 this 的值。
    • call()apply() 会立即调用函数,而 bind() 会创建一个新的函数,该函数在调用时会绑定指定的 this 值。
    • call()apply() 的区别在于,call() 接收参数列表,而 apply() 接收参数数组。
    1
    2
    3
    4
    5
    6
    7
    8
    
    function sayName() {
      console.log(this.name);
    }
    const obj = { name: 'Bob' };
    sayName.call(obj); // Bob
    sayName.apply(obj); // Bob
    const boundSayName = sayName.bind(obj);
    boundSayName(); // Bob
    
  4. new 绑定(new Binding)

    • 当使用 new 关键字调用构造函数时,会创建一个新的对象,并将 this 绑定到该对象。
    1
    2
    3
    4
    5
    
    function Person(name) {
      this.name = name;
    }
    const person = new Person('Charlie');
    console.log(person.name); // Charlie
    
  5. 箭头函数(Arrow Functions)

    • 箭头函数没有自己的 this,它会继承父作用域的 this
    • 箭头函数的 this 在定义时就已经确定,无法通过 call()apply()bind() 方法修改。
    1
    2
    3
    4
    5
    6
    7
    
    const obj = {
      name: 'David',
      sayName: () => {
        console.log(this.name); // 继承父作用域的 this
      }
    };
    obj.sayName(); // 在浏览器中:window.name,在 Node.js 中:undefined
    

优先级

当多个绑定规则同时适用时,它们的优先级如下:

  1. new 绑定
  2. 显式绑定
  3. 隐式绑定
  4. 默认绑定

注意事项

  • 在事件处理函数中,this 通常指向触发事件的 DOM 元素。
  • 在回调函数中,this 的值取决于回调函数的调用方式。
  • 可以使用 thatself 等变量来保存 this 的值,以便在回调函数中使用。

理解 this 的绑定规则对于编写正确的 JavaScript 代码至关重要。

原型链

原型链是 JavaScript 中实现继承的核心机制,它允许对象访问其原型对象的属性和方法。理解原型链对于深入理解 JavaScript 的面向对象编程至关重要。

1. 原型(Prototype)

  • 在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]],指向它的原型对象。
  • 可以通过 __proto__ 属性(非标准,但大多数浏览器支持)或 Object.getPrototypeOf() 方法来访问对象的原型。
  • 当试图访问对象的属性或方法时,如果对象自身没有该属性或方法,JavaScript 引擎会沿着原型链向上查找。

2. 构造函数(Constructor)

  • JavaScript 中的函数也可以作为构造函数使用,通过 new 关键字创建对象。
  • 每个构造函数都有一个 prototype 属性,指向它的原型对象。
  • 通过构造函数创建的对象,其 [[Prototype]] 属性会指向构造函数的 prototype 属性。

3. 原型链(Prototype Chain)

  • 原型对象也可以有自己的原型,这样就形成了一条原型链。
  • 当访问对象的属性或方法时,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(null)。
  • 原型链的顶端是 Object.prototype,它是所有对象的最终原型。

4. instanceof 运算符

  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
  • 如果出现在原型链上,则返回 true,否则返回 false

5. 示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function Person(name) {
  this.name = name;
}

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

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

console.log(person1.__proto__ === Person.prototype); // 输出:true
console.log(Person.prototype.__proto__ === Object.prototype); // 输出:true
console.log(Object.prototype.__proto__); // 输出:null

console.log(person1 instanceof Person); // 输出:true
console.log(person1 instanceof Object); // 输出:true

6. 总结

  • 原型链是 JavaScript 中实现继承的关键机制。
  • 通过原型链,对象可以访问其原型对象的属性和方法。
  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

掌握原型链对于理解 JavaScript 的面向对象编程至关重要。

事件循环

事件循环(Event Loop)是 JavaScript 实现异步编程的核心机制,它允许 JavaScript 在单线程环境下处理耗时操作,而不会阻塞主线程。

1. 为什么需要事件循环?

JavaScript 是一种单线程语言,这意味着它一次只能执行一个任务。如果遇到耗时的操作(例如网络请求、定时器、用户交互),主线程会被阻塞,导致页面卡顿或无响应。为了解决这个问题,JavaScript 引入了事件循环机制。

2. 事件循环的工作原理

事件循环的核心在于维护两个队列:

  • 宏任务队列(Macro Task Queue): 存放待执行的宏任务,例如 setTimeoutsetIntervalsetImmediate(Node.js)、requestAnimationFrame、I/O 操作、用户交互事件等。
  • 微任务队列(Micro Task Queue): 存放待执行的微任务,例如 Promise.thenMutationObserverprocess.nextTick(Node.js)等。

事件循环的工作流程如下:

  1. 执行同步代码:JavaScript 引擎首先执行主线程上的同步代码。
  2. 检查微任务队列:当主线程上的同步代码执行完毕后,JavaScript 引擎会检查微任务队列。
  3. 执行微任务:如果微任务队列中有任务,JavaScript 引擎会依次执行微任务队列中的所有任务,直到队列为空。
  4. 更新渲染:如果需要更新渲染,浏览器会进行页面渲染。
  5. 检查宏任务队列:JavaScript 引擎会检查宏任务队列。
  6. 执行宏任务:如果宏任务队列中有任务,JavaScript 引擎会从宏任务队列中取出一个任务执行。
  7. 重复步骤 2-6:JavaScript 引擎会不断重复步骤 2-6,直到宏任务队列和微任务队列都为空。

3. 宏任务和微任务的区别

  • 执行时机: 微任务的执行时机比宏任务更早。在每次宏任务执行完毕后,都会立即执行微任务队列中的所有任务。
  • 优先级: 微任务的优先级高于宏任务。
  • 类型: 宏任务和微任务的类型不同。

4. 示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
console.log('start');

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

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

console.log('end');

// 输出顺序:
// start
// end
// promise
// setTimeout

5. 总结

  • 事件循环是 JavaScript 实现异步编程的核心机制。
  • 它通过维护宏任务队列和微任务队列,实现了非阻塞的异步操作。
  • 理解事件循环对于编写高性能的 JavaScript 代码至关重要。

讲解一下深拷贝和浅拷贝,说出深拷贝和浅拷贝有几种方式?

在 JavaScript 中,深拷贝和浅拷贝都是用于复制对象或数组的技术,但它们在复制方式和结果上有所不同。

浅拷贝(Shallow Copy)

  • 浅拷贝是创建一个新对象或数组,然后将原始对象或数组的属性或元素复制到新对象或数组中。
  • 如果原始对象或数组的属性或元素是基本类型(如数字、字符串、布尔值),则复制的是值本身。
  • 如果原始对象或数组的属性或元素是引用类型(如对象、数组),则复制的是引用地址。
  • 这意味着,如果修改新对象或数组中的引用类型属性或元素,原始对象或数组也会受到影响。

浅拷贝的方式:

  • Object.assign()
    • 将一个或多个源对象的属性复制到目标对象中。
    • 只会复制第一层属性,如果属性是引用类型,则复制引用地址。
  • 展开运算符(Spread Operator):
    • 使用 ... 运算符可以展开对象或数组,并创建一个新的对象或数组。
    • Object.assign() 类似,只会复制第一层属性。
  • Array.prototype.slice()
    • 返回数组的一部分浅拷贝到一个新数组对象。原始数组不会被修改。
  • Array.prototype.concat()
    • 用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

深拷贝(Deep Copy)

  • 深拷贝是创建一个完全独立的新对象或数组,并将原始对象或数组的所有属性或元素(包括嵌套的引用类型)都复制到新对象或数组中。
  • 这意味着,修改新对象或数组不会影响原始对象或数组。

深拷贝的方式:

  • JSON.parse(JSON.stringify())
    • 将对象或数组转换为 JSON 字符串,然后再将 JSON 字符串解析为新的对象或数组。
    • 这是一种简单但有效的方法,但有一些限制:
      • 无法复制函数、undefinedSymbol
      • 无法复制循环引用的对象。
  • 递归函数:
    • 编写一个递归函数,遍历原始对象或数组的所有属性或元素,并将其复制到新对象或数组中。
    • 这是一种更灵活的方法,可以处理各种复杂的情况。
  • 使用第三方库:
    • 例如 lodash_.cloneDeep() 方法。

总结:

  • 浅拷贝复制引用,深拷贝复制值。
  • JSON.parse(JSON.stringify()) 是一种简单但有局限的深拷贝方法。
  • 递归函数是一种更灵活的深拷贝方法。
  • 使用第三方库可以简化深拷贝的实现。

希望这些信息能够帮助你理解深拷贝和浅拷贝。

你如何将 arguments 参数改变为数组

在 JavaScript 中,arguments 是一个类数组对象,它包含函数调用时传递的所有参数。虽然它看起来像一个数组,但它没有数组的所有方法。因此,我们需要将其转换为真正的数组,以便使用数组的方法进行操作。

以下是一些将 arguments 转换为数组的方法:

1. Array.prototype.slice.call(arguments)

  • 这是最常用的方法之一。
  • slice() 方法是数组的方法,它返回数组的一部分浅拷贝。
  • call() 方法允许我们将 slice() 方法应用于 arguments 对象。
1
2
3
4
5
6
function toArray() {
  return Array.prototype.slice.call(arguments);
}

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

2. Array.from(arguments)

  • Array.from() 方法从类数组对象或可迭代对象创建一个新的数组实例。
  • 这是 ES6 中引入的方法,更简洁易懂。
1
2
3
4
5
6
function toArray() {
  return Array.from(arguments);
}

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

3. 展开运算符 (...arguments)

  • 展开运算符可以将类数组对象或可迭代对象展开为参数列表。
  • 我们可以将其与数组字面量结合使用,创建一个新的数组。
  • 这也是 ES6 中引入的方法,非常简洁。
1
2
3
4
5
6
function toArray() {
  return [...arguments];
}

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

4. Object.values(arguments)

  • 如果函数没有命名参数,或者命名参数的数量和传入的参数数量不匹配,可以使用Object.values()方法。
  • 该方法会返回一个包含给定对象所有可枚举属性值的数组。
1
2
3
4
5
6
function toArray() {
  return Object.values(arguments);
}

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

选择哪种方法?

  • 如果需要兼容旧版本的浏览器,可以使用 Array.prototype.slice.call(arguments)
  • 如果使用 ES6 或更高版本,建议使用 Array.from(arguments) 或展开运算符,因为它们更简洁易懂。
  • Object.values(arguments)适用于参数数量不确定的情况。

希望这些信息能够帮助你理解如何将 arguments 参数转换为数组。

为什么typeof null 会是object,为什么null instanceof Object会是false

在 JavaScript 中,typeof null 返回 "object",而 null instanceof Object 返回 false,这是两个看似矛盾但实际上有历史和技术原因的现象。

1. typeof null 返回 "object" 的原因:

  • 历史原因:
    • 在 JavaScript 最初的实现中,值是由一个类型标签和实际数据值表示的。
    • 对象的类型标签是 0。
    • 由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签也是 0。
    • 这导致了 typeof null 返回 "object" 的结果。
    • 这是一个历史遗留问题,虽然 ECMAScript 标准承认这是一个错误,但为了保持向后兼容性,并没有修复它。
  • 技术原因:
    • JavaScript在底层存储变量的时候,会在变量的机器码的前三位存储类型信息。
    • 000被判断为object
    • null 的机器码全是0,因此typeof null的结果就是object

2. null instanceof Object 返回 false 的原因:

  • instanceof 的工作原理:
    • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
    • null 不是一个对象实例,它是一个特殊的值,表示“没有对象”。
    • instanceof 的左侧必须是一个对象,而 null 并不是一个对象,因此null instanceof Object 返回 false
  • null 的本质:
    • null 表示一个空的对象引用,它不指向任何对象。
    • 因此,null 没有原型链,也就无法通过 instanceof 检测。

总结:

  • typeof null 返回 "object" 是一个历史遗留的错误。
  • null instanceof Object 返回 false 是因为 null 不是一个对象实例,它没有原型链。

尽管 typeof null 的结果有些令人困惑,但在实际开发中,我们通常可以使用以下方法来准确判断一个值是否为 null

1
value === null;

希望这些解释能够帮助你理解这两个现象。

Licensed under CC BY-NC-SA 4.0
最后更新于 Mar 20, 2025 10:32 UTC