Skip to content

DRAFT 《现代 JavaScript 教程》前端基础系列 07:对象进阶——深入理解 this、构造函数与可选链

约 4394 字大约 15 分钟

2026-04-17

本文将深入探讨 JavaScript 对象的进阶特性,主要聚焦于对象方法的定义、this 关键字的动态绑定机制、利用构造函数批量创建对象的模式,以及使用可选链 ?. 安全访问深层嵌套属性的技巧。学完本篇,你将能够熟练运用面向对象的基础范式组织代码,并规避常见的 this 指向丢失与属性不存在等引发的报错坑点。

【本篇核心收获】

  • 掌握对象方法的定义与 this 机制:理解 this 的动态绑定原则,彻底避开 this 上下文丢失的陷阱。
  • 熟练使用箭头函数处理上下文:理解箭头函数没有自己 this 的特性及其在外部作用域获取 this 的应用场景。
  • 精通构造函数与 new 操作符:掌握使用构造函数批量创建对象的执行全流程,并透彻理解其内部隐式的 return 机制。
  • 灵活运用可选链 ?. 语法:学会优雅、安全地访问嵌套对象深层属性,告别繁琐的条件判断和因数据缺失导致的程序崩溃。

对象方法与 this 关键字

通常我们创建对象来表示真实世界中的实体,如用户和订单等。在现实世界中,用户可以进行各种操作:挑选商品、登录和注销等。在 JavaScript 中,对象的行为由属性中的函数来表示,作为对象属性的函数被称为方法

方法的定义与简写

刚开始,我们先通过将函数分配给对象的属性来教 user 说话:

let user = {
  name: "John",
  age: 30
};

// 分配方法
user.sayHi = function() {
  alert("Hello!");
};

user.sayHi(); // Hello!

我们也可以使用预先声明的函数作为方法,或者在对象字面量中直接进行声明。在实际开发中,我们更倾向于使用方法简写的语法,直接省略掉 function 关键字:

let user = {
  // 简写形式:等同于 "sayHi: function() {...}"
  sayHi() { 
    alert("Hello");
  }
};

面向对象编程 (OOP) 铺垫
当我们在代码中用对象表示实体,并为其绑定行为方法时,这种编程范式就是面向对象编程 (OOP)。选择合适的实体并组织它们交互的架构设计是一门极深的学问。

深入理解方法中的 this

通常,对象方法需要访问对象中存储的信息才能完成其工作(例如,使用 user 对象的 name 属性)。为了在方法内部访问该对象,需要使用 this 关键字。

this 的值就是“点操作符之前的这个对象”,即当前调用该方法的对象本身。

let user = {
  name: "John",
  age: 30,

  sayHi() {
    // "this" 指的是“当前的对象” user
    alert(this.name);
  }
};

user.sayHi(); // John

避坑指南:为什么不用外部变量名代替 this
从技术上讲,不使用 this 而直接通过外部变量名(如 user.name)访问也是可以的。但这非常不可靠。如果我们将 user 复制给另一个变量(如 admin = user),并把原变量 user 赋为 null,后续调用 admin.sayHi() 时就会因为内部依然硬编码访问 user.name 而导致报错:TypeError: Cannot read property 'name' of null。而使用 this.name 就可以完美避免这种上下文失效的问题。

this 的动态绑定机制(this 不受限制)

在 JavaScript 中,this 关键字与其他大多数编程语言不同。它可以被用于任何函数,即使该函数不是对象的方法。

function sayHi() {
  // 语法上完全合法
  alert( this.name );
}

this 的值是在代码运行时计算出来的,完全取决于调用时的代码上下文。相同的函数分配给不同的对象,在调用时会有不同的 this 值:

let user = { name: "John" };
let admin = { name: "Admin" };

function sayHi() {
  alert( this.name );
}

// 在两个对象中使用相同的函数
user.f = sayHi;
admin.f = sayHi;

// 这两个调用有不同的 this 值(函数内部的 "this" 是点符号前面的那个对象)
user.f(); // John(this == user)
admin.f(); // Admin(this == admin)

如果在没有对象绑定的情况下直接调用:

function sayHi() {
  alert(this);
}
sayHi(); // undefined (严格模式) 或 window/global (非严格模式)

在非严格模式下,无上下文调用时的 this 是全局对象(历史行为);而在开启严格模式("use strict")下,它会是 undefined。这种灵活性的代价就是极易因为错误的调用方式丢失 this 绑定导致报错。

特例:箭头函数没有自己的 this

箭头函数非常特别:它们没有自己的this。如果在箭头函数内部引用 this,它会向外层寻找,最终取外部“正常”函数的 this 上下文。

let user = {
  firstName: "Ilya",
  sayHi() {
    // 箭头函数直接取用外层 sayHi 方法的 this(即 user 对象)
    let arrow = () => alert(this.firstName);
    arrow();
  }
};

user.sayHi(); // Ilya

当我们不需要建立一个独立的 this 环境,而是想原封不动地继承外层上下文时,箭头函数是绝佳的选择。

实战落地:this 机制的典型应用场景

场景一:理解 this 在调用时的解析
访问以下代码中 ref 的结果是什么?为什么?

function makeUser() {
  return {
    name: "John",
    ref: this
  };
}
let user = makeUser();
alert( user.ref.name );

答案:会产生一个报错。 因为 this 的值并不在对象字面量定义时绑定,而是看调用方式。此处 makeUser() 是一次普通的函数调用(而非通过点符号调用),内部的 this 解析为 undefined。因此 user.ref 的值为 undefined,尝试读取 name 时报错。
正确方案:把 ref 改成一个方法,如 ref() { return this; }。这样通过 user.ref() 调用时,this 就绑定为点前面的 user 对象了。

场景二:创建一个状态存储计算器
创建一个有三个方法的 calculator 对象,分别负责输入、求和与求积:

let calculator = {
  sum() {
    return this.a + this.b;
  },
  mul() {
    return this.a * this.b;
  },
  read() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  }
};

calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );

场景三:实现高可读性的链式调用
通过在每个操作方法末尾返回当前对象本身(return this),即可实现如 jQuery 风格的链式调用:

let ladder = {
  step: 0,
  up() {
    this.step++;
    return this; // 返回对象本身
  },
  down() {
    this.step--;
    return this;
  },
  showStep() {
    alert( this.step );
    return this;
  }
};

// 由于返回了对象本身,可以连续打点调用
ladder.up().up().down().showStep().down().showStep(); // 展示 1,然后 0

模块小结:本模块探讨了如何通过方法为对象赋予行为,并重点剖析了 JavaScript 中 this 关键字的动态绑定机制。时刻牢记:常规函数中的 this 取决于调用时的对象(点前面的对象),而箭头函数没有自己的 this,它会直接向外层词法作用域寻找上下文。


构造函数与 new 操作符

常规的 {...} 语法允许创建一个对象。但是,在实际业务中我们经常需要批量创建大量有着相似结构的对象(如多个用户、多个商品等)。这可以通过构造函数new操作符来实现。

构造函数基础与执行全流程

构造函数在技术上就是常规函数。不过有两条共同的约定:

  1. 函数名首字母大写(如 User)。
  2. 它们只能由 new 操作符来调用执行。
function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");
alert(user.name); // Jack
alert(user.isAdmin); // false

核心原理拆解:当一个函数被使用 new 运行时,它在底层自动执行了以下四个步骤:

  1. 隐式创建一个全新的空对象,并分配给内部 this 指针:this = {}
  2. 开始执行函数体代码。
  3. 代码通过操作 this,为其添加新的属性或方法。
  4. 函数执行完毕,隐式返回修改完毕的 this 对象。

因此,new User("Jack") 的结果等同于返回了对象 { name: "Jack", isAdmin: false },实现了代码的高效复用。

封装复杂逻辑:匿名构造函数

如果我们有一些包含复杂初始化逻辑的代码,且仅用于构建单个复杂对象,我们可以将它们封装在一个立即执行的构造函数中:

let user = new function() {
  this.name = "John";
  this.isAdmin = false;

  // 这里可以写复杂的构建逻辑、包含局部变量等
};

这种技巧使得对象的创建代码被妥善封装在一个独立的作用域内,同时没有将构造函数暴露到外部以防被多次调用。

进阶探索:构造器模式测试 new.target

在一个函数内部,我们可以通过检查特殊的 new.target 属性,来获知该函数是否是使用 new 操作符调用的:

  • 对于常规调用,new.targetundefined
  • 对于使用 new 调用,它等于该函数自身。

库开发者常利用这一特性,使得构造函数在被遗漏 new 调用时,也能提供兜底并正常工作:

function User(name) {
  if (!new.target) { // 发现开发者没有通过 new 运行
    return new User(name); // 自动补全 new 并重定向调用
  }
  this.name = name;
}

let john = User("John"); // 即使忘了 new,也能正常创建
alert(john.name); // John

注:日常开发依然建议始终带上 new 关键字,以明确表达“在此创建新对象”的语义。

构造函数的 return 机制与方法添加

通常,构造器是不需要写 return 语句的,它们会自动返回填充了属性的 this 对象。但如果代码里强制写了 return,则遵循以下规则:

  1. 如果 return 返回的是一个对象,则原本的 this 被抛弃,直接返回该对象。
  2. 如果 return 返回的是一个原始类型,则该 return 语句被忽略,依然正常返回 this
function BigUser() {
  this.name = "John";
  return { name: "Godzilla" };  // <-- 强行返回一个新对象
}
alert(new BigUser().name);  // Godzilla,"John" 连同 this 被抛弃

此外,我们不仅可以在构造函数里向 this 挂载属性,还可以添加方法:

function User(name) {
  this.name = name;
  this.sayHi = function() {
    alert("My name is: " + this.name);
  };
}
let john = new User("John");
john.sayHi(); // My name is: John

避坑指南:省略括号的语法糖
如果构造函数不需要传入任何参数,我们可以省略 new 后面的括号,写成 let user = new User;。这和 new User() 完全等效,但不被认为是一种好风格。规范推荐始终保留括号。

实战落地:构造函数的典型应用场景

场景一:两个不同的函数返回相同对象
是否可以创建函数 AB,使得 new A() == new B() 成立?
答案:可以。 利用上面刚提到的 return 对象覆盖机制,让它们返回外部预先定义的同一个对象即可:

let obj = {};

function A() { return obj; }
function B() { return obj; }

alert(new A() == new B()); // true

场景二:创建基于构造函数的独立计算器实例
基于构造函数的模式,我们可以非常轻松地创建无数个互不干扰的 Calculator 对象实例:

function Calculator() {
  this.read = function() {
    this.a = +prompt('a?', 0);
    this.b = +prompt('b?', 0);
  };
  this.sum = function() {
    return this.a + this.b;
  };
  this.mul = function() {
    return this.a * this.b;
  };
}

let calculator = new Calculator();
calculator.read();
alert("Sum=" + calculator.sum());
alert("Mul=" + calculator.mul());

场景三:创建带有状态记忆的累加器
创建一个构造函数 Accumulator(startingValue),内部通过 value 保存累加总和:

function Accumulator(startingValue) {
  // 设置初始状态
  this.value = startingValue;

  this.read = function() {
    // 每次通过 prompt 获取增量,累加进 value
    this.value += +prompt('How much to add?', 0);
  };
}

let accumulator = new Accumulator(1); // 初始值设为 1
accumulator.read(); 
accumulator.read(); 
alert(accumulator.value); // 展示最终累加和

模块小结:构造函数本质上是首字母大写的普通函数,通过 new 操作符调用时,会隐式创建并返回一个新的对象实例。掌握这一底层机制是深入 JavaScript 面向对象编程基石的关键,它极大地方便了我们批量构建具有相同属性和方法的同类对象。


可选链 ?. 安全访问嵌套属性

传统方案解决“不存在的属性”的痛点

在处理层级较深的用户数据或由于网络请求带来的非确定性数据时,常常会遇到“访问不存在的嵌套属性”引发的错误:

let user = {}; // 这个 user 没有 address 属性
alert(user.address.street); // TypeError Error! 代码在此处崩溃

类似的问题也常出现在 DOM 元素查询中:

// 如果查找不到元素返回 null,接着尝试读取 .innerHTML 就会报错
let html = document.querySelector('.elem').innerHTML;

以往我们的做法是使用条件运算符 ? 或是逻辑与 && 提前进行一层一层的校验拦截:

// 方案 A:三元运算符
alert(user.address ? user.address.street : undefined);

// 方案 B:&& 逻辑运算检查
alert(user.address && user.address.street && user.address.street.name);

这两种方案都能避免程序崩溃,但它们存在共同的问题:代码过度重复、阅读体验极其糟糕(如对象变量名反复书写),并且嵌套越深,代码越丑陋。

可选链的基础用法与短路效应

可选链 ?. 是解决上述痛点的绝佳语法特性:如果 ?. 前面的值为 undefined 或者 null,它会立即停止后续运算并返回 undefined

let user = {}; // 无 address 属性
// 安全访问:因为 user.address 不存在,立刻中止并返回 undefined
alert(user?.address?.street); // undefined(静默失败,程序不崩溃)

结合 DOM 搜索的优雅写法:

let html = document.querySelector('.elem')?.innerHTML; // 找不到元素就优雅返回 undefined

短路效应机制
可选链不仅仅是阻止报错,它还具备**“短路效应”。如果 ?. 左边计算出不存在,不仅链式取值会断开,右侧挂载的任何函数调用或自增操作均不会执行**:

let user = null;
let x = 0;

user?.sayHi(x++); // 因没有 user,不仅跳过了 sayHi,连传参处的 x++ 也被阻止了

alert(x); // 0,值完全没有增加

避坑指南:切勿过度滥用可选链
我们应该只将 ?. 使用在明确允许不存在的属性上。如果你的代码逻辑认为 user 对象一定存在(例如接口已明确返回 user),只有 address 是选填项,那就应该写成 user.address?.street,而不是 user?.address?.street。过度滥用会使得原本应在前端抛出的数据逻辑错误被隐藏,给后期 Debug 带来灾难。
另外,?. 左侧的基底变量(如 user)必须在作用域内用 let/const/var 已声明过,否则依然会报 ReferenceError is not defined

其他语法变体:?.()?.[]

可选链 ?. 并不是一个普通的运算符,而是一个特殊的语法结构,它可以与函数括号 () 和方括号 [] 完美结合。

1. ?.():安全调用可选方法
当你无法确定某个对象身上是否绑定了某个方法时,可以使用 ?.() 安全调用:

let userAdmin = {
  admin() { alert("I am admin"); }
};
let userGuest = {};

userAdmin.admin?.(); // I am admin
userGuest.admin?.(); // 什么都不会发生,安全跳过

2. ?.[]:安全读取动态属性
在使用方括号通过变量读取对象属性时,也可以使用 ?.[]

let key = "firstName";
let user1 = { firstName: "John" };
let user2 = null;

alert(user1?.[key]); // John
alert(user2?.[key]); // undefined

3. 结合 delete 操作符
甚至可以用来安全地移除深层对象的属性:

delete user?.name; // 如果 user 存在,则删除内部的 name 属性

避坑指南:可选链禁止用于赋值操作
可选链 ?. 只能用于安全地读取、函数调用或删除。它绝对不能用在赋值语句的等号左侧,这会导致直接爆出语法错误。

let user = null;
user?.name = "John"; // SyntaxError! (因为 JavaScript 无法执行 undefined = "John")

模块小结:可选链 ?. 是极其优雅的语法糖,能够避免由于中间层级属性缺失带来的程序直接崩溃。它拥有 ?.prop?.[]?.() 三种变体形态,自带向右截断的短路效应。但切记它仅限读取与删除场景,禁止用于赋值,并且不能盲目滥用以掩盖真实的代码逻辑错误。


【本篇核心知识点速记】

  • 对象方法与 this 机制:对象内部用于表达动作的函数称为方法。常规方法的 this 指向在调用时动态决定(永远指向“点符号前面的对象”);而箭头函数没有独立的this,它会跳过自身直接使用外部词法环境的上下文。
  • 构造函数与 new 机制:通过 new Fn() 调用普通函数时,会触发四大隐式动作(建空对象 → 指向 this → 执行代码 → 返回 this)。通过构造函数能大批量制造独立的对象实例。如果有强制的 return {obj} 语句,原始的 this 就会被覆盖抛弃。
  • 可选链 ?. 机制:当探测到左侧的值为 nullundefined 时,会直接阻断该表达式右侧的一切执行(短路效应),并静默返回 undefined。它配合 ?.() 可安全调用不确定的方法,配合 ?.[] 可安全读取深层动态键,是防御性编程处理层级数据查询的绝佳利器。
贡献者: weew12