阅读视图

发现新文章,点击刷新页面。

JavaScript 深拷贝与浅拷贝详解

前言

在 JavaScript 开发中,拷贝对象是一个非常常见的操作。
但很多时候你以为“复制成功了”,其实只是复制了引用,这就会引发很多 Bug。

所以必须搞清楚:

  • 什么是浅拷贝
  • 什么是深拷贝
  • 常见实现方式有哪些

一、浅拷贝是什么?

浅拷贝只会复制对象的第一层属性。
如果属性值还是对象,那么复制的仍然是引用地址。

const obj1 = {
  name: 'Tom',
  info: {
    age: 18
  }
};

const obj2 = { ...obj1 };

obj2.info.age = 20;

console.log(obj1.info.age); // 20

说明obj1.infoobj2.info指向同一个对象。


二、常见浅拷贝方式

1)展开运算符

const obj2 = { ...obj1 };

2)Object.assign

const obj2 = Object.assign({}, obj1);

这两种都属于浅拷贝。


三、深拷贝是什么?

深拷贝会递归复制对象的每一层,生成完全独立的新对象。

const obj1 = {
  name: 'Tom',
  info: {
    age: 18
  }
};

const obj2 = JSON.parse(JSON.stringify(obj1));

obj2.info.age = 20;

console.log(obj1.info.age); // 18

四、JSON 深拷贝的问题

虽然:

JSON.parse(JSON.stringify(obj))

很常见,但它有局限:

  • 不能拷贝函数

  • 不能拷贝 undefined

  • 不能拷贝Symbol

  • 不能处理循环引用

  • DateRegExp 会丢失信息


五、手写一个简单深拷贝

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  const result = Array.isArray(obj) ? [] : {};

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = deepClone(obj[key]);
    }
  }

  return result;
}

六、总结

浅拷贝和深拷贝最核心的区别是:

  • 浅拷贝:只拷贝第一层
  • 深拷贝:递归拷贝所有层级

实际开发中要根据场景选择,不要把浅拷贝误当成深拷贝。

JavaScript call、apply、bind 详解

前言

callapplybind是 JavaScript 中用来显式改变this的三个常用方法。 它们既是开发中常见工具,也是面试高频考点。


一、为什么需要 call、apply、bind?

有时候我们希望一个函数在执行时,this指向指定对象。

例如:

function say() {
  console.log(this.name);
}

const obj = {
  name: 'Tom'
};

如果直接调用:

say();

此时this不会指向obj。所以我们需要:

say.call(obj);

二、call

call会立即执行函数,并且第一个参数就是指定的this

function say(age) {
  console.log(this.name, age);
}

const obj = { name: 'Tom' };

say.call(obj, 18); // Tom 18

三、apply

applycall类似,也会立即执行函数。区别在于参数形式不同:

  • call:一个一个传参数

  • apply :参数放在数组里传

function say(age, city) {
  console.log(this.name, age, city);
}

const obj = { name: 'Tom' };

say.apply(obj, [18, 'Beijing']); // Tom 18 Beijing

四、bind

bind不会立即执行函数,而是返回一个新的函数,这个新函数的

this已经被绑定好了。

function say(age) {
  console.log(this.name, age);
}

const obj = { name: 'Tom' };

const fn = say.bind(obj, 18);
fn(); // Tom 18

五、三者区别总结

相同点

  • 都可以改变函数执行时的 this

不同点

call

  • 立即执行
  • 参数逐个传

apply

  • 立即执行
  • 参数数组传

bind

  • 不立即执行
  • 返回新函数

六、常见使用场景

1)借用方法

const arrLike = { 0: 'a', 1: 'b', length: 2 };
const arr = Array.prototype.slice.call(arrLike);

console.log(arr); // ['a', 'b']

2)绑定 this

const obj = {
  name: 'Tom',
  say() {
    console.log(this.name);
  }
};

setTimeout(obj.say.bind(obj), 1000);

七、总结

callapplybind的核心作用就是:

显式指定函数执行时的 this。

记忆口诀:

  • call :立即调用,参数逐个传

  • apply:立即调用,参数数组传

  • bind:不立即调用,返回新函数

JavaScript 作用域与作用域链详解

前言

在 JavaScript 中,作用域是非常基础但又非常重要的知识点。
闭包、变量查找、函数执行、模块化等内容,都和作用域密切相关。

本文主要讲清楚:

  • 什么是作用域
  • JavaScript 有哪些作用域
  • 什么是作用域链
  • 变量查找规则是什么

一、什么是作用域?

作用域可以理解为:

变量和函数可以被访问的范围。

也就是说,一个变量不是在任何地方都能访问,它的可访问范围由作用域决定。

例如:

let a = 10;

function test() {
  let b = 20;
  console.log(a); // 可以访问
}

test();
// console.log(b); // 报错

这里:

  • a 在全局作用域中

  • b 在函数作用域中

  • 函数内部可以访问全局变量

  • 全局不能访问函数内部变量


二、JavaScript 中的几种作用域

1)全局作用域

定义在函数外部的变量,通常属于全局作用域。

let name = 'Tom';

function say() {
  console.log(name);
}

全局作用域中的变量,在当前脚本中通常都可以访问。


2)函数作用域

在函数内部声明的变量,只能在函数内部访问。

function test() {
  let age = 18;
  console.log(age);
}

test();
// console.log(age); // 报错

3)块级作用域

使用letconst声明的变量,会形成块级作用域。

{
  let a = 1;
  const b = 2;
}

// console.log(a); // 报错
// console.log(b); // 报错

ifforwhile{}都可以形成块级作用域。


三、var、let、const 的作用域区别

var

  • 没有块级作用域
  • 只有全局作用域和函数作用域
if (true) {
  var a = 10;
}

console.log(a); // 10

let / const

  • 有块级作用域
if (true) {
  let b = 20;
}

// console.log(b); // 报错

四、什么是作用域链?

当在当前作用域中查找某个变量时,如果找不到,就会去上一级作用域查找,直到全局作用域。
这个逐级向上查找的过程,就叫做 作用域链


五、作用域链示例

let a = 1;

function outer() {
  let b = 2;

  function inner() {
    let c = 3;
    console.log(“结果”,a, b, c);
  }

  inner();
}

outer();

输出:

结果,1 2 3

查找过程:

  • c:先在inner 自己内部找,找到

  • binner找不到,去outer 找,找到

  • ainner找不到,outer 找不到,去全局找,找到

这就是作用域链。


六、作用域链的本质

作用域链的本质是:

函数在定义时,就已经确定了它能访问哪些外部变量。

注意,是定义时,不是调用时。

这也是闭包能成立的基础。


七、总结

作用域决定了变量的可访问范围,作用域链决定了变量的查找路径。

重点记住:

  • 全局作用域
  • 函数作用域
  • 块级作用域
  • 变量查找是逐级向上找
  • 找不到最终会报错

JavaScript this 指向详解

前言

在 JavaScript 中,this是一个非常高频、也非常容易让人混乱的知识点。
很多初学者会发现:同样是一个函数,在不同场景下调用this的值居然不一样。

比如:

  • 普通函数里的this

  • 对象方法里的this

  • 构造函数里的this

  • 箭头函数里的this

  • callapplybind改变this指向

本文就来系统讲清楚:JavaScript 中 this 到底指向谁。


一、this 是什么?

this不是在函数定义时决定的,而是在函数调用时决定的

也就是说:

谁调用这个函数,this 通常就指向谁。

先看一个简单例子:

const obj = {
  name: 'Tom',
  say() {
    console.log(this.name);
  }
};

obj.say(); // Tom

这里say()是被obj调用的,所以this指向obj


二、普通函数中的 this

1)浏览器非严格模式下

function test() {
  console.log(this);
}

test();

在浏览器非严格模式下,普通函数直接调用时,

this指向window


2)严格模式下

'use strict';

function test() {
  console.log(this);
}

test();

严格模式下,普通函数直接调用时,

thisundefined


三、对象方法中的 this

如果函数作为对象的方法调用,那么this指向这个对象。

const obj = {
  name: 'Alice',
  say() {
    console.log(this.name);
  }
};

obj.say(); // Alice

注意,真正决定this的,不是函数写在哪,而是怎么调用

const obj = {
  name: 'Alice',
  say() {
    console.log(this.name);
  }
};

const fn = obj.say;
fn(); // 浏览器非严格模式下通常是 undefined 或 window.name

这里fn()已经不是通过obj调用了,所以this不再指向obj


四、构造函数中的 this

当函数通过new调用时,this指向新创建的实例对象。

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

const p = new Person('Tom');
console.log(p.name); // Tom

这里this指向p


五、箭头函数中的 this

箭头函数没有自己的this,它的this取决于外层作用域

const obj = {
  name: 'Tom',
  say: () => {
    console.log(this.name);
  }
};

obj.say();

这里箭头函数不会绑定obj,而是继承外层的this


如果在浏览器全局环境下,通常指向window


一个更容易理解的例子

const obj = {
  name: 'Tom',
  say() {
    const fn = () => {
      console.log(this.name);
    };
    fn();
  }
};

obj.say(); // Tom

这里箭头函数fn继承了say()this,也就是obj


六、事件中的 this

在 DOM 事件中,普通函数里的this一般指向触发事件的元素。

button.onclick = function () {
  console.log(this); // button 元素
};

如果写成箭头函数:

button.onclick = () => {
  console.log(this);
};

那么这里的this不再指向按钮,而是继承外层作用域。


七、call、apply、bind 改变 this

JavaScript 提供了三种显式改变this的方法:

  • call
  • apply
  • bind
function say() {
  console.log(this.name);
}

const obj = { name: 'Tom' };

say.call(obj);  // Tom
say.apply(obj); // Tom

bind 不会立即执行,而是返回一个新的函数:

const fn = say.bind(obj);
fn(); // Tom

八、this 指向总结

可以简单记住下面几条:

  1. 普通函数直接调用:浏览器非严格模式下指向 window

  2. 对象方法调用:指向调用它的对象

  3. 构造函数调用:指向新实例

  4. 箭头函数:没有自己的 this ,继承外层

  5. call/apply/bind:可以显式指定 this


九、总结

this的核心不是“函数定义在哪”,而是:

函数是如何被调用的。

只要抓住这句话,再结合几种常见场景,this就不会再那么抽象了。

JavaScript 模块化详解:CommonJS、ES Module 到底有什么区别?

前言

当项目代码越来越多时,如果所有变量、函数、对象都写在一个文件中,就会带来很多问题:

  • 容易命名冲突
  • 不方便协作
  • 代码难维护
  • 功能难复用

所以 JavaScript 需要 模块化

模块化并不只是“把代码拆分成多个文件”,更重要的是:

让每个文件都有自己的作用域,并且可以通过导入和导出组织代码。

本文就来系统讲清楚 JavaScript 模块化的发展和常见方案。


一、为什么需要模块化?

早期 JavaScript 没有模块系统,大家通常把变量和函数直接写到全局作用域中:

var name = 'Tom';

function getUser() {
  return name;
}

如果多个文件都写同名变量,就容易冲突。

模块化的出现,就是为了解决这些问题:

  • 避免全局污染
  • 提高代码复用
  • 方便拆分文件
  • 便于团队协作
  • 提高可维护性

二、早期模块化:IIFE

在 ES Module 出现之前,经常用立即执行函数来模拟模块作用域。

const userModule = (function () {
  let name = 'Tom';

  function getName() {
    return name;
  }

  function setName(newName) {
    name = newName;
  }

  return {
    getName,
    setName
  };
})();

console.log(userModule.getName()); // Tom

这种方式本质上利用了 闭包

特点:

  • 可以创建私有作用域
  • 能避免部分全局污染
  • 但不够标准,维护成本高

三、CommonJS

CommonJS 是 Node.js 中常见的模块化规范。

导出

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

module.exports = {
  add
};

导入

// app.js
const math = require('./math');

console.log(math.add(1, 2)); // 3

特点:

  • 主要用于 Node.js

  • 使用

    require

    导入

  • 使用

    module.exports

    导出

  • 同步加载


四、ES Module(ESM)

这是现在最主流、最推荐的模块化方案。

命名导出

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

export function sub(a, b) {
  return a - b;
}

导入:

import { add, sub } from './math.js';

console.log(add(1, 2));
console.log(sub(5, 3));

默认导出

// user.js
export default function getUser() {
  return { name: 'Tom' };
}

导入:

import getUser from './user.js';

console.log(getUser());

五、命名导出和默认导出的区别

命名导出

export const name = 'Tom';

导入时必须使用对应名字:

import { name } from './file.js';

默认导出

export default function () {}

导入时名字可以自定义:

import myFn from './file.js';

六、CommonJS 和 ES Module 的区别

CommonJS

const math = require('./math');
module.exports = { add };

ES Module

import { add } from './math.js';
export { add };

常见区别:

  • CommonJS 多用于 Node.js 传统环境
  • ES Module 是 JavaScript 官方标准模块系统
  • CommonJS 是同步加载
  • ES Module 更适合现代前端工程化

七、模块化的实际意义

模块化最大的价值是让代码更清晰。

比如一个项目可以拆成:

src
├── api
├── utils
├── components
├── views
└── store

每个模块负责不同功能,代码更容易维护。


八、总结

JavaScript 模块化的核心目标是:

把代码拆分成独立、可维护、可复用的模块。

学习模块化时,重点记住:

  • 早期有 IIFE

  • Node.js 常见 CommonJS

  • 现代前端主流是 ES Module

  • export / import是最常见写法

原型与原型链

前言

在 JavaScript 中,原型是一个非常重要的概念。
如果不理解原型,很多内容都会变得很模糊,比如:

  • 构造函数
  • 实例方法共享
  • 原型链
  • 类的本质

本文重点讲清楚:什么是原型、为什么需要原型、prototype 和 proto 有什么区别、原型链


一、什么是原型?

在 JavaScript 中,每个函数都有一个特殊属性:

prototype

这个属性指向一个对象,这个对象就叫做 原型对象

看一个例子:

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

Person.prototype.sayHello = function () {
  console.log(`你好,我是 ${this.name}`);
};

const p1 = new Person('Tom');
const p2 = new Person('Alice');

p1.sayHello(); // 你好,我是 Tom
p2.sayHello(); // 你好,我是 Alice

这里:

Person.prototype

就是构造函数

Person

的原型对象。


二、为什么需要原型?

如果我们把方法写在构造函数内部:

function Person(name) {
  this.name = name;
  this.sayHello = function () {
    console.log(`你好,我是 ${this.name}`);
  };
}

那么每创建一个实例,就会重新创建一次

sayHello

方法。

这样会导致:

  • 方法重复创建
  • 浪费内存

如果写到原型上:

Person.prototype.sayHello = function () {
  console.log(`你好,我是 ${this.name}`);
};

那么所有实例就可以共享同一个方法。

所以原型最核心的作用就是:

让实例共享属性和方法。


三、prototype 和 proto 的区别

这是非常高频、也非常容易混淆的知识点。

1)prototype

  • 只有函数才有
  • 指向构造函数的原型对象

2)__proto__

  • 对象才有
  • 指向该对象的原型

看例子:

function Person() {}

const p = new Person();

console.log(Person.prototype);
console.log(p.__proto__);
console.log(p.__proto__ === Person.prototype); // true

这个关系非常重要:

实例对象.__proto__ === 构造函数.prototype


四、原型上的属性和实例上的属性

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

Person.prototype.age = 18;

const p = new Person('Tom');

console.log(p.name); // Tom
console.log(p.age);  // 18

这里:

  • name

    是实例自己的属性

  • age

    是原型上的属性

当访问

p.age

时,JS 发现实例本身没有这个属性,就会去原型上找。


五、原型的实际意义

原型的最大意义就是“共享”。

例如数组为什么都有

push

pop

map

这些方法?

因为这些方法都定义在:

Array.prototype

上。

所以数组实例本身不需要重复拥有这些方法。


六、总结

原型可以用一句话总结:

原型是 JavaScript 中实现属性和方法共享的机制。

重点记住:

  • 函数有

    prototype

  • 对象有

    __proto__

  • 实例.__proto__ === 构造函数.prototype


学完原型之后,接下来最重要的就是 原型链
原型链本质上解决的是一个问题:

当我们访问对象属性时,JavaScript 到底是怎么查找的?

理解了原型链,你就能更清楚地看懂:

  • 为什么对象可以调用某些方法

  • 为什么数组能用

    push

  • 为什么实例可以访问原型方法


原型链

一、什么是原型链?

当访问一个对象的属性或方法时,JavaScript 会先在对象本身查找。
如果找不到,就会去对象的原型上查找。
如果原型上还找不到,就继续去原型的原型上查找。
这一层一层向上查找的结构,就叫做 原型链


二、属性查找过程

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

Person.prototype.sayHello = function () {
  console.log(`你好,我是 ${this.name}`);
};

const p = new Person('Tom');

console.log(p.name); // Tom
p.sayHello();        // 你好,我是 Tom

查找p.name

  1. 先看

    p

    自身有没有

    name

  2. 有,直接返回

查找p.sayHello

  1. 先看

    p

    自身有没有

    sayHello

  2. 没有

  3. p.__proto__

    ,也就是

    Person.prototype

    上找

  4. 找到了,执行它


三、原型链的尽头是什么?

function Person() {}

const p = new Person();

console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null

所以原型链大致是:

p
→ Person.prototypeObject.prototypenull

null

就是原型链的终点。


四、数组的原型链

数组也是对象,所以也有原型链。

const arr = [1, 2, 3];

console.log(arr.__proto__ === Array.prototype); // true
console.log(Array.prototype.__proto__ === Object.prototype); // true

原型链结构大致是:

arr
→ Array.prototypeObject.prototypenull

所以数组既能用数组方法,也能用对象原型上的某些方法。


五、为什么原型链重要?

因为 JavaScript 中很多方法并不是对象自身直接拥有的,而是通过原型链继承来的。

例如:

const arr = [1, 2, 3];

arr.push(4);
arr.toString();

这里:

  • push

    来自

    Array.prototype

  • toString

    可能继续来自更上层原型


六、总结

原型链可以简单理解为:

对象查找属性时,沿着原型一层层向上查找的链式结构。

查找规则:

  1. 先找对象自身

  2. 再找原型

  3. 再找原型的原型

  4. 直到

    null

❌