1. 对象创建:从“能用”到“精通”的必经之路
在JavaScript的世界里,对象是当之无愧的基石。无论是构建一个简单的配置项,还是设计一个复杂的应用状态树,亦或是实现面向对象的继承体系,都离不开对象的创建。很多开发者,尤其是刚入门的同学,可能只熟悉一两种创建对象的方式,比如最直观的字面量或者new Object()。但当你深入阅读开源库的源码,或者尝试构建更健壮、更灵活的代码结构时,会发现不同的创建方式背后,隐藏着截然不同的设计意图、性能考量和适用场景。掌握这七种方式,不仅仅是多学几个API,更是理解JavaScript语言特性、内存管理和设计模式的关键一步。它能让你在代码评审时一眼看出同事的用意,在性能优化时找到更优的路径,在设计架构时拥有更多的选择。今天,我们就来彻底拆解这七种创建JavaScript对象的方法,从最基础的到最高级的,从原理到实践,让你不仅知道怎么写,更明白为什么要这么写。
2. 七种创建方式全景解析与核心思路
在深入每一种方法之前,我们有必要先建立一个全局视角。JavaScript对象的创建方式,大致可以沿着两条轴线来理解:一是“声明方式”,二是“原型关系”。声明方式决定了你书写代码的语法和直观性,而原型关系则决定了对象在内存中的继承链和行为共享机制。这七种方法并非彼此孤立,它们之间存在演进和互补的关系。
核心思路的演进:从最简单的直接封装数据(对象字面量),到需要批量生成相似结构(工厂模式、构造函数),再到需要更精细地控制对象行为和继承(Object.create、ES6 Class),最后到动态和元编程的领域(Object.assign、代理与反射)。这个演进过程,实际上对应着前端应用从简单的页面交互,到复杂单页应用,再到如今大型工程化项目的发展历程。理解这一点,你就能明白在什么场景下该选用哪种工具,而不是机械地记忆语法。
方案选型的考量:选择哪种方式,通常基于以下几个关键点:
- 简洁性与可读性:对于简单的数据聚合,字面量是无敌的。
- 复用性与批量创建:当需要创建多个具有相同结构或行为的对象时,构造函数或Class是首选。
- 继承与原型链控制:当你需要建立清晰的继承关系,或刻意创建一个没有原型的“纯净”字典时,
Object.create提供了最根本的能力。 - 对象组合与混入:现代开发中,组合优于继承的理念深入人心,
Object.assign是实现对象属性混合的关键工具。 - 元编程与高级控制:需要对对象的底层操作(如属性读取、赋值、枚举)进行拦截和自定义时,
Proxy是唯一的答案。
下面,我们就按照从基础到高级的顺序,逐一拆解这七种方式,我会在每种方式中详细解释其原理、步骤、坑点以及最适用的场景。
3. 方式一:对象字面量——最直观的起点
这是几乎所有JavaScript学习者接触到的第一种,也是最常用的一种创建对象的方式。它的语法极其简单,使用一对花括号{}来定义对象及其属性。
3.1 基本语法与核心原理
const person = { name: '张三', age: 30, greet() { console.log(`你好,我是${this.name}`); } };从原理上讲,当你执行这段代码时,JavaScript引擎会在堆内存中创建一个新的普通对象。这个对象的内部原型([[Prototype]])指向Object.prototype。name和age是其自身的属性(own property),而greet方法也是一个自身属性,只不过它的值是一个函数。
为什么它如此高效和流行?
- 语法糖:字面量语法是创建对象语法糖,它比等价的
new Object()写法更简洁、解析更快。 - 声明即初始化:你可以在创建对象的同时,直接定义其所有属性和方法,代码紧凑,意图清晰。
- JSON兼容:对象字面量的语法是JSON格式的超集(JSON不允许函数和undefined),这使得它与现代Web API的数据交换无缝衔接。
3.2 实操要点与常见陷阱
虽然简单,但使用时也有几个必须注意的细节:
1. 属性名标识符:
- 有效的标识符(如
name,age_1)可以直接作为键。 - 如果属性名包含空格、连字符,或者希望使用数字、保留字作为开头,则必须使用引号包裹,成为字符串键。
const weirdObj = { 'first-name': 'John', // 必须加引号 '123': '数字开头', // 必须加引号 for: '这是一个保留字' // 严格模式下不建议,但加引号后可定义 };在ES6之后,如果属性名来自一个变量,你可以使用计算属性名:
const dynamicKey = 'id'; const obj = { [dynamicKey + ‘Number’]: 123 // 计算属性名 };2. 方法定义的演变:在ES6之前,定义方法必须使用完整的函数表达式:
const oldWay = { sayHi: function() { console.log(‘Hi’); } };ES6引入了更简洁的方法定义语法,如上文的greet(),它除了写法简洁,还有一个细微差别:它内部是具名的,便于调试,且支持super关键字。
3. 原型链的固定性:字面量创建的对象,其原型固定为Object.prototype。你无法在声明时直接指定一个自定义的原型。这是它与后面Object.create方式的主要区别。
注意:对象字面量创建的是单例对象。如果你需要基于同一个“蓝图”创建多个实例,比如创建10个
person,手动写10个字面量显然是不现实的。这时就需要我们接下来要讲的构造函数或工厂模式。
4. 方式二:Object构造函数——历史的痕迹
在对象字面量语法普及之前,new Object()是标准做法。现在它看起来有些过时,但了解它有助于理解语言的历史和某些边缘情况。
4.1 基本用法与字面量等价
const person = new Object(); person.name = ‘张三’; person.age = 30; person.greet = function() { console.log(`你好,我是${this.name}`); };这段代码与上一节的字面量方式在功能上是完全等价的。最终都会生成一个原型为Object.prototype的对象。
4.2 为什么如今不推荐作为首选?
- 冗余和繁琐:相比字面量
{},它需要更多的代码(new、Object()、分步赋值)。 - 性能微差:虽然现代JS引擎优化得非常好,但理论上,字面量语法在解析阶段更易于优化。
- 存在陷阱:
Object构造函数有一个特殊行为,根据传入参数的类型,它可能会返回一个非普通对象。
const obj1 = new Object(123); // 等价于 new Number(123),返回一个Number对象 console.log(typeof obj1); // “object” console.log(obj1 instanceof Number); // true const obj2 = new Object(‘text’); // 等价于 new String(‘text’),返回String对象 const obj3 = new Object(null); // 返回一个空对象 {} const obj4 = new Object(undefined); // 返回一个空对象 {}这个行为是为了保持与原始类型包装器的一致性,但在日常对象创建中,这极易造成混淆和意外。你本想创建一个普通对象,却可能得到一个String或Number的实例。
适用场景:在现代开发中,直接使用new Object()的场景极少。唯一可能用到的情况是,为了代码风格统一,或者在必须使用构造函数语法的框架中(但这类框架通常会有自己的基类)。对于99%的情况,请坚持使用对象字面量{}。
5. 方式三:工厂模式——封装创建逻辑
当我们需要创建多个结构相似的对象时,重复书写字面量会带来大量的代码冗余。工厂模式的核心思想是:用一个函数来封装对象的创建过程。
5.1 实现一个基础的工厂函数
function createPerson(name, age) { const person = {}; // 或者 new Object() person.name = name; person.age = age; person.greet = function() { console.log(`你好,我是${this.name}`); }; return person; } const person1 = createPerson(‘张三’, 30); const person2 = createPerson(‘李四’, 25);优点:
- 解决了代码复用问题:创建逻辑只写一次。
- 提供了创建对象的接口,调用者无需关心内部实现细节。
5.2 核心缺陷与内存问题
工厂模式有一个致命的缺点:每个对象都拥有自己独立的方法副本。 在上面的例子中,person1.greet和person2.greet是两个不同的函数对象,它们占用两份独立的内存空间。如果有成千上万个实例,这种内存浪费是巨大的。
console.log(person1.greet === person2.greet); // false优化方案:共享方法为了解决这个问题,我们可以将函数定义在工厂函数外部,然后在内部通过引用赋值。
function greet() { console.log(`你好,我是${this.name}`); } function createPerson(name, age) { const person = {}; person.name = name; person.age = age; person.greet = greet; // 引用同一个函数 return person; } const person1 = createPerson(‘张三’, 30); const person2 = createPerson(‘李四’, 25); console.log(person1.greet === person2.greet); // true,内存得到优化但这又引入了新的问题:
- 全局命名空间污染:
greet函数暴露在全局。 - 结构松散:对象和其方法在代码结构上是分离的,不利于封装和维护。
工厂模式虽然解决了批量创建的问题,但没有建立起对象之间的内在联系(原型链)。这引出了我们对更强大机制的需求——构造函数模式。
6. 方式四:构造函数模式——引入原型链
构造函数模式是JavaScript实现面向对象编程的基石。它利用了函数的new操作符和prototype属性。
6.1new操作符背后的四步魔法
当你使用new关键字调用一个函数时,后台发生了以下四件事:
- 创建一个全新的空对象。
- 将这个新对象的
[[Prototype]](内部原型)链接到构造函数的prototype属性指向的对象。 - 将构造函数内部的
this绑定到这个新创建的对象。 - 执行构造函数内部的代码(通常是为
this添加属性)。 - 如果构造函数没有显式返回一个对象,则自动返回这个新创建的对象。
function Person(name, age) { // 步骤3: this 指向新对象 {} this.name = name; this.age = age; // 步骤4: 执行代码,为 this 添加属性 // 步骤5: 默认返回 this } // 步骤2: Person.prototype 将成为新对象的原型 Person.prototype.greet = function() { console.log(`你好,我是${this.name}`); }; // 步骤1 & 整体过程: 创建对象并绑定原型 const person1 = new Person(‘张三’, 30); const person2 = new Person(‘李四’, 25); console.log(person1.greet === person2.greet); // true,方法共享了! console.log(person1 instanceof Person); // true,建立了实例关系6.2 原型链的构建与内存优势
通过将方法定义在Person.prototype上,所有通过new Person()创建的实例,其[[Prototype]]都指向同一个Person.prototype对象。当调用person1.greet()时,JS引擎首先在person1自身属性上查找greet,没找到,就沿着原型链找到Person.prototype上的greet方法。这样,方法在内存中只有一份,被所有实例共享,完美解决了工厂模式的内存浪费问题。
构造函数的约定:
- 函数名首字母大写,以区别于普通函数。
- 使用
new关键字调用。如果忘记使用new,函数内的this在非严格模式下会指向全局对象(如window),造成属性泄露;在严格模式下this为undefined,导致报错。
6.3 模拟实现一个new函数
理解new的原理后,我们可以自己实现一个简易版的myNew:
function myNew(constructorFn, ...args) { // 1. 创建一个新对象,并将其原型指向构造函数的prototype const obj = Object.create(constructorFn.prototype); // 2. 执行构造函数,并将this绑定到新对象 const result = constructorFn.apply(obj, args); // 3. 判断构造函数是否返回了一个对象,如果是则返回该对象,否则返回新创建的对象 return result instanceof Object ? result : obj; } // 测试 const person3 = myNew(Person, ‘王五’, 28); person3.greet(); // 输出:你好,我是王五 console.log(person3 instanceof Person); // true这个模拟实现清晰地揭示了new、prototype和this之间的关系。
7. 方式五:Object.create()——原型式继承的基石
Object.create()是ES5引入的一个非常强大的方法。它允许你以一个现有对象为原型,创建一个新对象。这是实现纯原型继承的最直接方式。
7.1 创建具有指定原型的对象
const personPrototype = { greet() { console.log(`你好,我是${this.name}`); } }; const person1 = Object.create(personPrototype); person1.name = ‘张三’; // 添加自身属性 person1.age = 30; person1.greet(); // “你好,我是张三” console.log(Object.getPrototypeOf(person1) === personPrototype); // trueObject.create(personPrototype)创建了一个空对象,并将这个空对象的原型设置为personPrototype。然后我们再为这个新对象添加自身的属性。
7.2 第二个参数:属性描述符
Object.create()的第二个参数是一个对象,用于定义新对象的自身属性,并且可以精确控制属性的特性(可枚举、可配置、可写)。
const person2 = Object.create(personPrototype, { name: { value: ‘李四’, writable: true, // 可写 enumerable: true, // 可枚举(例如,for…in循环) configurable: true // 可配置(例如,可删除) }, age: { value: 25, enumerable: true } });这种方式在需要创建属性不可变、隐藏等特殊对象时非常有用。
7.3 核心应用场景与注意事项
创建纯净的“字典”对象:
Object.create(null)可以创建一个没有任何原型([[Prototype]]为null)的对象。它不继承Object.prototype上的任何方法(如toString,hasOwnProperty)。这非常适合用作纯粹的键值对存储,可以避免原型上的属性名与自定义键名冲突。const pureDict = Object.create(null); pureDict[‘toString’] = ‘myValue’; // 安全,不会覆盖方法 console.log(‘toString’ in pureDict); // true实现原型链继承:在ES6的
class语法之前,这是实现继承的推荐方式之一(结合构造函数)。function Animal(name) { this.name = name; } Animal.prototype.speak = function() {}; function Dog(name, breed) { Animal.call(this, name); // 继承属性 this.breed = breed; } // 继承方法:将Dog的原型指向以Animal.prototype为原型的对象 Dog.prototype = Object.create(Animal.prototype); // 修复constructor指针 Dog.prototype.constructor = Dog; Dog.prototype.bark = function() {}; const myDog = new Dog(‘Rex’, ‘Husky’);
注意:直接使用
Object.create()创建的对象,其原型链是动态的。如果你修改了原型对象(personPrototype),所有基于它创建的对象都会受到影响。这既是强大的灵活性,也带来了需要小心管理的耦合性。
8. 方式六:ES6 Class——语法糖与现代化
ES6引入的class关键字,本质上是构造函数和原型继承的语法糖。它没有引入新的面向对象继承模型,只是让代码更清晰、更易读,更接近传统面向对象语言的写法。
8.1 Class语法详解
class Person { // 构造函数 constructor(name, age) { this.name = name; this.age = age; } // 实例方法(定义在 Person.prototype 上) greet() { console.log(`你好,我是${this.name}`); } // 静态方法(定义在 Person 构造函数本身上) static compareAge(personA, personB) { return personA.age - personB.age; } // Getter get description() { return `${this.name} (${this.age}岁)`; } // Setter set nickname(value) { this._nickname = value; } } const person1 = new Person(‘张三’, 30); const person2 = new Person(‘李四’, 25); person1.greet(); // 实例方法 console.log(Person.compareAge(person1, person2)); // 静态方法 console.log(person1.description); // Getter person1.nickname = ‘小张’; // Setter8.2 与构造函数模式的对比
| 特性 | 构造函数模式 | ES6 Class |
|---|---|---|
| 语法 | 函数声明,手动操作prototype | 使用class、constructor等关键字,更清晰 |
| 方法定义 | Person.prototype.method = function() {} | 直接在类体内定义,更简洁 |
| 继承 | 需要手动组合Object.create、call和修正constructor | 使用extends和super关键字,语法直观 |
| 静态方法 | Person.staticMethod = function() {} | 使用static关键字声明 |
| 访问器属性 | 需使用Object.defineProperty | 直接使用get和set语法 |
| 提升 | 函数声明会提升 | Class 声明不会提升(存在暂时性死区) |
Class的本质:typeof Person的结果是“function”。Person本质上仍然是一个构造函数。类中定义的所有方法都是不可枚举的(enumerable: false),而用构造函数手动添加到prototype上的方法默认是可枚举的。这是一个细微但重要的区别。
8.3 使用继承:extends与super
class语法让继承变得异常简单和直观:
class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise.`); } } class Dog extends Animal { constructor(name, breed) { super(name); // 必须首先调用 super() this.breed = breed; } speak() { super.speak(); // 可以调用父类方法 console.log(`${this.name} barks!`); } } const myDog = new Dog(‘Rex’, ‘Husky’); myDog.speak();extends关键字建立了原型链。super在构造函数中用于调用父类的构造函数,在方法中用于调用父类的方法。在子类的constructor中,必须先调用super(),才能使用this,这是因为子类实例的创建依赖于父类实例的初始化。
9. 方式七:Object.assign()与动态组合
前面几种方式主要关注对象的“创建”和“继承”。而Object.assign()的核心用途是对象的合并与属性复制,常用于基于现有对象创建新对象,或者实现混入模式。
9.1 基本用法:属性合并
const target = { a: 1 }; const source1 = { b: 2 }; const source2 = { c: 3, a: 100 }; // a会覆盖target中的a const result = Object.assign(target, source1, source2); console.log(result); // { a: 100, b: 2, c: 3 } console.log(target); // { a: 100, b: 2, c: 3 },注意!target本身也被修改了Object.assign()将后续所有源对象(source1,source2)自身的、可枚举的属性,复制到第一个目标对象(target)上。它返回目标对象。关键点:这是一个浅拷贝,且会修改目标对象。
9.2 创建新对象与混入模式
为了不修改现有对象,我们通常以一个空对象{}作为目标:
const defaultSettings = { theme: ‘light’, fontSize: 14 }; const userSettings = { fontSize: 16, showSidebar: true }; const finalSettings = Object.assign({}, defaultSettings, userSettings); console.log(finalSettings); // { theme: ‘light’, fontSize: 16, showSidebar: true } console.log(defaultSettings); // { theme: ‘light’, fontSize: 14 },未被修改这常用于合并配置项、状态更新等场景。
混入模式:通过Object.assign()可以将多个对象的功能组合到一个对象中,这是一种替代类继承的轻量级代码复用方式。
const canEat = { eat() { console.log(`${this.name} is eating.`); } }; const canWalk = { walk() { console.log(`${this.name} is walking.`); } }; function createPerson(name) { return Object.assign({ name }, canEat, canWalk); } const person = createPerson(‘John’); person.eat(); person.walk();9.3 重要限制:浅拷贝的陷阱
Object.assign()只进行浅拷贝。如果源对象的属性值是一个对象(数组、函数、普通对象),那么复制的是这个对象的引用。
const obj = { nested: { a: 1 } }; const copy = Object.assign({}, obj); copy.nested.a = 999; console.log(obj.nested.a); // 999!原始对象也被修改了对于需要深拷贝的场景,Object.assign()无能为力,需要使用其他方法,如递归拷贝、JSON.parse(JSON.stringify(obj))(有局限性)或使用 lodash 的_.cloneDeep。
10. 高级方式:Proxy与Reflect——元编程的威力
严格来说,Proxy不是直接“创建”一个传统意义上的对象,而是创建一个对象的代理。这个代理可以拦截并重新定义该对象的基本操作(如属性查找、赋值、枚举、函数调用等)。它提供了强大的元编程能力。
10.1 创建代理对象
const target = { name: ‘张三’, age: 30 }; const handler = { get(obj, prop) { console.log(`正在读取属性:${prop}`); // 可以返回一个计算值,甚至不存在的属性 if (prop === ‘greeting’) { return `你好,${obj.name}`; } return prop in obj ? obj[prop] : ‘属性不存在’; }, set(obj, prop, value) { console.log(`正在设置属性:${prop} = ${value}`); if (prop === ‘age’ && typeof value !== ‘number’) { throw new TypeError(‘年龄必须是数字’); } obj[prop] = value; // 实际执行赋值 return true; // 表示设置成功 } }; const proxyPerson = new Proxy(target, handler); console.log(proxyPerson.name); // 日志:正在读取属性:name -> 输出:张三 console.log(proxyPerson.greeting); // 日志:正在读取属性:greeting -> 输出:你好,张三 console.log(proxyPerson.unknown); // 日志:正在读取属性:unknown -> 输出:属性不存在 proxyPerson.age = 31; // 日志:正在设置属性:age = 31 // proxyPerson.age = ‘old’; // 会抛出 TypeErrorhandler对象是一个“陷阱”集合,包含了各种拦截操作的方法(如get,set,has,deleteProperty等)。
10.2 核心应用场景
- 数据验证与拦截:如上例,在
set陷阱中验证数据类型、范围。 - 实现私有属性:通过拦截
get和set,可以模拟私有变量。const privateStore = new WeakMap(); const handler = { get(obj, prop) { if (prop.startsWith(‘_’)) { throw new Error(`无法访问私有属性 ${prop}`); } return obj[prop]; }, set(obj, prop, value) { if (prop.startsWith(‘_’)) { throw new Error(`无法设置私有属性 ${prop}`); } obj[prop] = value; return true; } }; - 观察者模式/数据绑定:在属性被修改时(
set陷阱),自动触发更新视图的函数。 - 实现负索引数组:拦截数组的
get操作,使得arr[-1]可以访问最后一个元素。 - 函数调用劫持:如果代理目标是函数,还可以拦截
apply和construct操作。
10.3 与Reflect的搭配使用
Reflect对象提供了与Proxy陷阱方法一一对应的静态方法。在陷阱处理程序中使用Reflect方法是一个最佳实践,因为它提供了操作对象的默认行为。
const handler = { set(obj, prop, value) { console.log(`设置 ${prop}`); // 使用 Reflect.set 执行默认的赋值操作 return Reflect.set(obj, prop, value); } };这样做的好处是保持了操作的规范性,并且Reflect方法总是返回一个布尔值表示操作是否成功,更易于处理。
注意事项:Proxy功能强大,但也会带来性能开销和调试复杂性。除非确实需要元编程能力,否则应优先使用更简单的对象创建方式。
11. 七种方式对比与选型指南
现在,我们已经全面了解了七种创建对象的方式。让我们通过一个表格来快速回顾和对比:
| 方式 | 核心特点 | 原型链控制 | 适用场景 | 注意事项 |
|---|---|---|---|---|
| 1. 对象字面量 | 语法简洁,声明式 | 固定为Object.prototype | 单例配置、简单数据结构、JSON | 无法复用结构 |
| 2. Object构造函数 | 历史遗留,函数调用 | 固定为Object.prototype | 极少数需统一风格的场景 | 冗余,有类型转换陷阱 |
| 3. 工厂模式 | 封装创建逻辑 | 无(或需手动设置) | 需要隐藏创建细节,但无需继承 | 方法内存浪费,需手动优化 |
| 4. 构造函数模式 | 使用new,结合prototype | 通过prototype属性控制 | 需要创建多个相似实例,建立类型体系 | 需用new,否则this指向错误 |
| 5. Object.create() | 直接指定原型对象 | 完全可控,可设为null | 纯净字典、原型式继承、精细属性控制 | 原型动态关联,需注意副作用 |
| 6. ES6 Class | 语法糖,现代化OOP | 通过extends控制 | 现代项目,清晰的类与继承结构 | 本质仍是函数和原型 |
| 7. Object.assign() | 对象合并,浅拷贝 | 取决于源对象 | 配置合并、混入模式、状态更新 | 会修改目标对象,浅拷贝陷阱 |
| 8. Proxy | 拦截对象基本操作 | 代理目标对象的原型 | 数据验证、观察者、元编程、高级抽象 | 性能开销,增加复杂度 |
选型决策流:
- 是否需要创建多个相似对象?
- 否-> 使用对象字面量。
- 是-> 进入第2步。
- 是否需要经典的继承关系和方法共享?
- 是-> 使用ES6 Class(首选)或构造函数模式。
- 否,只是结构相似-> 可以考虑工厂模式或使用
Object.create()配合一个原型对象。
- 是否需要一个完全没有原型的对象?
- 是-> 使用
Object.create(null)。
- 是-> 使用
- 是否需要基于现有对象组合出新对象?
- 是-> 使用
Object.assign({}, …sources)。
- 是-> 使用
- 是否需要拦截或自定义对象的基本操作(如读写属性)?
- 是-> 使用Proxy。
- 否-> 回到以上步骤。
12. 常见问题与排查技巧实录
在实际开发中,围绕对象创建会遇到各种各样的问题。这里记录了几个最典型的“坑”和解决思路。
12.1 构造函数忘记使用new关键字
问题现象:属性被添加到全局对象(如window)上,或者(严格模式下)报错Cannot set property ‘xxx’ of undefined。
function Person(name) { this.name = name; } const p = Person(‘张三’); // 糟糕!忘了写 new console.log(window.name); // ‘张三’ (非严格模式) console.log(p); // undefined解决方案:
- 代码规范:强制使用首字母大写的构造函数命名,并配合代码检查工具(如 ESLint)的
new-cap规则。 - 安全构造函数模式:在构造函数内部判断
this的类型。function Person(name) { if (!(this instanceof Person)) { return new Person(name); // 如果没通过new调用,自动补上 } this.name = name; } - 使用 ES6 Class:
Class语法必须通过new调用,否则直接报错,更安全。
12.2 原型方法中的this丢失
问题场景:将对象方法作为回调函数传递时。
class Timer { constructor(name) { this.name = name; } start() { setInterval(this.log, 1000); // 错误!this.log 中的 this 将指向全局或 undefined } log() { console.log(`Timer: ${this.name}`); // this 不对了! } }解决方案:
- 使用箭头函数:箭头函数不绑定自己的
this,会捕获其所在上下文的this值。class Timer { constructor(name) { this.name = name; this.log = () => { // 将方法定义为实例属性(箭头函数) console.log(`Timer: ${this.name}`); }; } start() { setInterval(this.log, 1000); // 现在 this 正确了 } } - 使用
bind方法:显式绑定this。start() { setInterval(this.log.bind(this), 1000); } - 使用类字段语法(ES2022+):这是目前最简洁的方式。
class Timer { name = ‘’; log = () => { console.log(`Timer: ${this.name}`); }; constructor(name) { this.name = name; } start() { setInterval(this.log, 1000); } }
12.3 循环引用导致JSON.stringify或深拷贝失败
问题现象:对象A的属性引用了对象B,对象B的属性又引用了对象A,形成循环引用。使用JSON.stringify会报错,简单的深拷贝递归会栈溢出。
const objA = { name: ‘A’ }; const objB = { name: ‘B’, partner: objA }; objA.partner = objB; // 循环引用 // JSON.stringify(objA); // TypeError: Converting circular structure to JSON排查与解决:
- 使用特殊的序列化库:如
flatted库,它可以处理循环引用。 - 实现支持循环引用的深拷贝:在递归拷贝过程中,使用一个
WeakMap来存储已拷贝过的对象。function deepClone(obj, hash = new WeakMap()) { if (obj === null || typeof obj !== ‘object’) return obj; if (hash.has(obj)) return hash.get(obj); // 如果已拷贝过,直接返回 const clone = Array.isArray(obj) ? [] : {}; hash.set(obj, clone); // 记录当前对象 for (let key in obj) { if (obj.hasOwnProperty(key)) { clone[key] = deepClone(obj[key], hash); } } return clone; }
12.4 判断对象类型时instanceof与isPrototypeOf的差异
instanceof:检查构造函数的prototype属性是否出现在对象的原型链上。它对原始类型无效(如‘str’ instanceof String为false)。Object.prototype.isPrototypeOf():检查一个对象是否存在于另一个对象的原型链上。更直接,且可用于任何对象。
function Foo() {} const f = new Foo(); console.log(f instanceof Foo); // true console.log(Foo.prototype.isPrototypeOf(f)); // true console.log(Object.prototype.isPrototypeOf(f)); // true const obj = Object.create(null); console.log(obj instanceof Object); // false,因为原型链为null console.log(Object.prototype.isPrototypeOf(obj)); // false选择建议:在需要判断自定义类型实例时,两者皆可。当需要判断一个对象是否以另一个对象为原型时,使用isPrototypeOf。判断内置类型时,Array.isArray()等比instanceof更可靠。
掌握这七种创建对象的方式,就像是掌握了JavaScript面向对象编程的七种武器。从简单的数据容器到复杂的代理拦截,每一种工具都在其特定的场景下发挥着不可替代的作用。理解它们的原理和差异,能让你在编码时做出更合理的选择,写出更高效、更健壮、更易维护的代码。真正的熟练,不是记住所有语法,而是在面对具体问题时,能下意识地选出最趁手的那一把。