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操作符来实现。
构造函数基础与执行全流程
构造函数在技术上就是常规函数。不过有两条共同的约定:
- 函数名首字母大写(如
User)。 - 它们只能由
new操作符来调用执行。
function User(name) {
this.name = name;
this.isAdmin = false;
}
let user = new User("Jack");
alert(user.name); // Jack
alert(user.isAdmin); // false核心原理拆解:当一个函数被使用 new 运行时,它在底层自动执行了以下四个步骤:
- 隐式创建一个全新的空对象,并分配给内部
this指针:this = {}。 - 开始执行函数体代码。
- 代码通过操作
this,为其添加新的属性或方法。 - 函数执行完毕,隐式返回修改完毕的
this对象。
因此,new User("Jack") 的结果等同于返回了对象 { name: "Jack", isAdmin: false },实现了代码的高效复用。
封装复杂逻辑:匿名构造函数
如果我们有一些包含复杂初始化逻辑的代码,且仅用于构建单个复杂对象,我们可以将它们封装在一个立即执行的构造函数中:
let user = new function() {
this.name = "John";
this.isAdmin = false;
// 这里可以写复杂的构建逻辑、包含局部变量等
};这种技巧使得对象的创建代码被妥善封装在一个独立的作用域内,同时没有将构造函数暴露到外部以防被多次调用。
进阶探索:构造器模式测试 new.target
在一个函数内部,我们可以通过检查特殊的 new.target 属性,来获知该函数是否是使用 new 操作符调用的:
- 对于常规调用,
new.target为undefined。 - 对于使用
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,则遵循以下规则:
- 如果
return返回的是一个对象,则原本的this被抛弃,直接返回该对象。 - 如果
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()完全等效,但不被认为是一种好风格。规范推荐始终保留括号。
实战落地:构造函数的典型应用场景
场景一:两个不同的函数返回相同对象
是否可以创建函数 A 和 B,使得 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]); // undefined3. 结合 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就会被覆盖抛弃。 - 可选链
?.机制:当探测到左侧的值为null或undefined时,会直接阻断该表达式右侧的一切执行(短路效应),并静默返回undefined。它配合?.()可安全调用不确定的方法,配合?.[]可安全读取深层动态键,是防御性编程处理层级数据查询的绝佳利器。
