适合读者:已掌握函数、作用域与变量提升,想把「对象建模」与「运行时 this」打通成一条完整知识链的学习者。
学习路线:先能灵活操作普通对象 → 再能批量生产实例 → 最后把 this 指向规则刻进肌肉记忆。
技术参考MDN Web DocsECMAScript® 2026 规范web.dev JavaScript 系列


目录


1. 知识脉络总览

1.1 核心知识地图

Object 字面量 / 属性操作

构造函数与实例化

this 的三种典型场景

包装对象与原型链预告

遍历 for...in / delete / in

instanceof / constructor

宿主丢失 / 全局 window

自定义构造函数

面试高频题

1.2 对象操作全景

Object 对象

创建方式

属性读写

遍历 for...in

删除 delete

存在性 in

字面量 {}

Object()

new Object()

点语法 obj.key

括号语法 obj['key']

属性名来自变量时必用

1.3 构造函数与 new 流程

否/原始值

调用 new Constructor()

① 分配内存,创建空对象

② 设置 [[Prototype]] 为 Constructor.prototype

③ 绑定 this 到新对象

④ 执行构造函数体

⑤ return 了对象类型?

返回 return 的对象(替换实例)

返回新建实例(默认路径)

1.4 this 指向决策树

直接调用 fn()

方法调用 obj.fn()

new Constructor()

fn.call/apply/bind

函数被调用

调用方式?

非严格: this = globalThis(window)

this = 点号左侧对象

this = 新建实例

this = 手动指定

严格模式: this = undefined

嵌套对象: 看最近的调用者

赋值后单独调用 → 宿主丢失!

1.5 一句话核心结论

主题 核心要点
Object 对象 键值集合;点/括号双语法;for...in 遍历可枚举属性;delete 删除;in 检测
构造函数 new 创建独立实例;return 原始值被忽略,return 对象替换默认实例
this 看调用方式:全局/普通调用、new、方法调用——三种最核心
包装对象 new Number 与数字直接量不同;临时包装「用完即毁」,无法持久挂属性
instanceof 检查原型链,而非简单的「谁创建了谁」

2. 名词解释与辨析

2.1 广义对象 vs 狭义 Object

  • 广义对象(口语层面):在 JavaScript 里,数组、函数、Date 等都能用属性语法操作,统称「对象」。
  • 狭义 Object 类型:语言层面的一种具体对象种类,与 ArrayFunctionDate 等并列;通常用 {}new Object() 创建。

辨析[] 本质是 Array 的实例;function(){} 本质是 Function 的实例。它们几乎总是同时满足 x instanceof Object(因为原型链上都有 Object.prototype),除非用 Object.create(null) 刻意断掉原型。

Object.prototype

Array.prototype

[ ]

Function.prototype

function(){}

Date.prototype

new Date()

普通对象 { }

2.2 属性(property)与方法(method)

  • 属性name: value 形式的键值对,value 可以是任意类型。
  • 方法:约定俗成地把值为函数的属性叫作方法,如 obj.sayHello()。对 JavaScript 引擎来说,它仍是普通属性。
var product = {
  title: "Wireless Keyboard",  // 属性:字符串
  price: 89,                   // 属性:数字
  tags: ["electronics", "input-device"], // 属性:数组
  describe: function() {       // 方法:值为函数
    return this.title + " — $" + this.price;
  }
};

2.3 实例化(Instantiation)

new Constructor() 制造一个新对象,并在构造过程中完成初始化。每次 new 对应一块独立的实例内存,不同实例之间互不干扰。

工厂隐喻:构造函数是生产模具,new 是启动生产线,每次运行产出一个独立产品。

2.4 构造函数(Constructor Function)

  • 系统内置ObjectArrayFunctionDateRegExpError
  • 自定义:普通函数配合 new 使用,承担「类」的角色(ES2015 的 class 语法是其语法糖)。

规范要点:instanceof 判断的是原型链上是否出现某构造函数的 prototypeMDN:instanceof)。

2.5 constructor 属性

obj.constructor 可追溯到创建它的构造函数。注意:这是原型链上的可覆盖信息,不能作为绝对可靠的类型判断。更稳妥的选择:Array.isArray()Object.prototype.toString.call()

var arr = [];
console.log(arr.constructor === Array);  // true
console.log(arr.constructor === Object); // false(Array 优先)

// 更稳健的类型判断
console.log(Object.prototype.toString.call(arr)); // "[object Array]"
console.log(Object.prototype.toString.call({}));  // "[object Object]"
console.log(Object.prototype.toString.call(42));  // "[object Number]"

2.6 全局对象与 window

  • 浏览器中,全局对象是 window(现代标准统一入口:globalThisMDN:globalThis)。
  • 顶层 var 声明的变量在非模块脚本中会成为全局对象的属性,因此改 this.x(普通调用时)等于改全局变量。

2.7 this 的本质

this运行时绑定的关键字,不是编译期写死的值。函数如何被调用决定 this 是谁,与函数书写在哪里无关。

MDN 描述:this 是与执行上下文(execution context)相关的关键字(MDN:this)。

2.8 包装对象(Wrapper Object)

StringNumberBoolean 三种原始类型,当你对它们访问属性/方法时,JavaScript 引擎会临时创建对应的包装对象,操作完立即销毁。这就是为什么字符串能调用 .toUpperCase() 但无法持久存储属性。

2.9 可枚举属性(Enumerable Property)

for...in 只遍历可枚举的属性(包括继承的)。通过 Object.defineProperty 设置 enumerable: false 的属性不会被遍历到——这是数组 length 不出现在 for...in 中的原因之一。

2.10 原型链(Prototype Chain)

每个对象都有一个内部链接 [[Prototype]],指向其构造函数的 prototype 属性。属性查找失败时沿链向上,直到 null 为止。这是 instanceof 的实现基础,也是下一章的核心主题。


3. Object 对象:创建、读写、遍历、删除、存在性判断

3.1 创建对象的三种方式

方式 ① 对象字面量(推荐,最简洁)
var profile = {
  username: "Alice",         // 属性名合法标识符,可省略引号
  age: 28,                   // 值可以是任意类型
  "home-address": "Riverside Ave",  // 含连字符,属性名必须加引号
  tags: ["frontend", "typescript"], // 值是数组——对象可以嵌套任意结构
  address: {                 // 值是另一个对象——嵌套对象
    city: "Metro City",
    zip: "10001"
  },
  greet: function() {        // 值是函数——该属性称为"方法"
    return "Hello, " + this.username; // this 指向调用该方法的对象
  }
};

💡 代码解析:

  • username: "Alice" — 标准键值对,属性名符合标识符规则时引号可省略
  • "home-address" — 含 - 特殊字符,必须用引号包裹,否则语法错误
  • tags: [...] — 属性值可以是数组、对象、函数等任意类型
  • address: {...} — 嵌套对象,访问时链式调用 profile.address.city
  • greet: function(){} — 值是函数,此属性习惯上称为"方法",内部 this 指向调用者
方式 ② Object() 函数调用
var obj1 = Object();      // 不传参数:创建空普通对象,等价于 {}
var obj2 = Object(null);  // 传 null/undefined:同样返回空对象
var obj3 = Object(42);    // 传原始值:将其包装成对应的对象类型(Number 包装对象)
console.log(obj3 instanceof Number); // true —— obj3 是 Number 包装对象而非原始数字

💡 代码解析:

  • Object() 作为普通函数调用(无 new)时,行为取决于参数:
    • 无参数 / null / undefined → 返回新的空对象 {}
    • 传入原始值(数字/字符串/布尔) → 返回对应的包装对象
    • 传入对象 → 直接返回该对象本身(不创建新对象)
方式 ③ new Object() 构造函数
var obj = new Object(); // 显式用构造函数创建空对象,与 {} 效果相同
obj.name = "Bob";       // 动态添加属性——对象是开放结构,可随时添加
obj.score = 95;         // 再添加一个数值属性

💡 代码解析:

  • new Object(){} 完全等价,日常开发优先用字面量 {}
  • 对不存在的属性赋值,会自动创建该属性(对象是动态的键值集合)

工程建议:字面量语法更简洁、性能无差异,是日常开发的首选。

3.2 属性的读取与写入

var car = { brand: "Tesla", model: "Model S", year: 2024 };

// 点语法读取(属性名合法标识符时)
console.log(car.brand);    // "Tesla" —— 最常用,代码最简洁

// 括号语法读取(通用,接受任意字符串)
console.log(car["model"]); // "Model S" —— 与点语法完全等价(属性名为字符串字面量时)

// 变量作为属性名——必须用括号,点语法做不到
var key = "year";
console.log(car[key]);     // 2024 —— car["year"] 的动态写法,key 是变量

// 读取不存在的属性 → undefined(不报错!)
console.log(car.color);    // undefined —— 属性不存在时安全返回 undefined

// 赋值给不存在的属性 → 自动创建该属性(对象是动态的)
car.color = "red";
console.log(car.color);    // "red" —— 属性已被动态添加到 car 上

// 特殊属性名(含连字符/空格等)只能用括号
var opts = { "data-id": 101, "max-retry": 3 };
console.log(opts["data-id"]);  // 101 —— opts.data-id 会被解析成减法运算,报错或得 NaN

💡 代码解析:

代码 说明
car.brand 点语法,属性名必须是合法标识符(字母/数字/_/$,不以数字开头)
car["model"] 括号语法,属性名可以是任意字符串,包括特殊字符
car[key] 动态属性名,key 是一个变量,运行时才确定属性名
car.colorundefined 访问不存在的属性不报错,返回 undefined;这与访问未声明的变量(报 ReferenceError)不同
car.color = "red" 赋值写法,若属性不存在则自动创建;这是 JS 对象"动态性"的体现
opts["data-id"] - 的属性名无法用点语法,opts.data-id 被解析为 opts.data - id
必须用 [] 的两种场景
场景 示例
属性名含特殊字符(连字符、空格等) obj["home-address"]
属性名来自变量 obj[fieldName]

3.3 遍历属性:for...in

var server = {
  host: "localhost",
  port: 3000,
  protocol: "http",
  timeout: 5000
};

// for...in 语法:变量 key 依次取每个可枚举属性的"名字"(字符串类型)
for (var key in server) {
  // key 是属性名字符串:"host" / "port" / "protocol" / "timeout"
  // server[key] 用括号语法动态读取对应属性值(不能用 server.key,那会找名为 key 的属性)
  console.log(key + " => " + server[key]);
}
// host => localhost
// port => 3000
// protocol => http
// timeout => 5000

💡 代码解析:

  • for (var key in server)key 每次循环被赋为一个属性名字符串
  • server[key] — 必须用括号语法,因为 key 是变量;若写 server.key 则查找名为 "key" 的属性(结果 undefined
  • for...in 遍历的是可枚举属性,包括从原型链继承的;length、内置方法等不可枚举属性不会出现
遍历注意事项

for...in

自有可枚举属性

继承的可枚举属性(原型链)

❌ 不遍历不可枚举属性

❌ 不遍历 Symbol 键

// 只想遍历自有属性,用 hasOwnProperty 过滤继承属性
for (var key in server) {
  // hasOwnProperty 判断属性是否为对象"自身"拥有,而非继承自原型
  // 写法:Object.prototype.hasOwnProperty.call(obj, key) 比 obj.hasOwnProperty(key) 更安全
  // (避免对象自身覆盖了 hasOwnProperty 方法的极端情况)
  if (Object.prototype.hasOwnProperty.call(server, key)) {
    console.log(key, server[key]);
  }
}

// 现代 API(ES2022):Object.hasOwn 是 hasOwnProperty 的更简洁替代
for (var key in server) {
  if (Object.hasOwn(server, key)) { // 效果同上,但写法更简洁安全
    console.log(key, server[key]);
  }
}

// 最佳实践:直接用 Object.keys() 只返回自有可枚举键的数组,避免 for...in 的原型链问题
Object.keys(server).forEach(function(key) {
  console.log(key, server[key]);
});
// Object.keys 返回 ["host", "port", "protocol", "timeout"],forEach 逐个处理

💡 代码解析:

  • hasOwnProperty 的必要性:如果给 Object.prototype 添加了自定义属性,for...in 会遍历到它;用 hasOwnProperty 可过滤掉继承的属性
  • Object.prototype.hasOwnProperty.call(server, key) — 通过 call 借用 Object.prototype 上的方法,防止对象自身覆盖 hasOwnProperty
  • Object.keys() — 只返回自有可枚举属性名的数组,是最常用的现代替代方案

数组遍历建议:数组优先用 for(按索引)、for...of(值)或 Array 方法(forEachmap)——for...in 遍历数组会混入自定义属性名,容易踩坑。

3.4 删除属性:delete

var config = {
  debug: true,
  version: "1.0.0",
  secret: "abc123",
  timeout: 3000
};

// delete 运算符:从对象中彻底移除属性
delete config.secret;       // 用点语法删除(属性名为合法标识符时)
delete config["debug"];     // 用括号语法删除(等效,特殊属性名时必用)
console.log(config); // { version: "1.0.0", timeout: 3000 } —— secret 和 debug 已消失

// delete 一个不存在的属性:不报错,返回 true(操作"成功"的含义是"属性不存在了")
console.log(delete config.notExist); // true

// ⚠️ 数组使用 delete 的陷阱:只删除值,留下"空洞",length 不变
var arr = [10, 20, 30, 40];
delete arr[1];              // 删除索引1的元素,但槽位还在
console.log(arr);           // [10, empty, 30, 40] —— 索引1变成空洞
console.log(arr.length);    // 4 ← length 未变!这是 delete 与 splice 的关键区别
console.log(arr[1]);        // undefined ← 空洞读取为 undefined

// ✅ 正确做法:用 splice 真正移除元素,会自动调整 length
arr.splice(2, 1); // 参数:(起始索引, 删除个数),此处从索引2删除1个元素
console.log(arr); // [10, empty, 40] —— length 变为 3

💡 代码解析:

  • delete obj.key — 返回 true 代表操作完成(属性不存在也返回 true),返回 false 代表删除失败(属性不可配置时)
  • delete arr[1] — 数组索引本质也是属性,delete 后索引变为空洞(稀疏数组),length 不变,读取得 undefined
  • arr.splice(index, count) — 真正移除元素,相邻元素向前移位,length 相应减小,这才是移除数组元素的正确方式
delete 的限制
// ❌ 无法删除 var 声明的全局变量(var 声明的变量 configurable: false)
var globalVar = 100;
delete globalVar;           // 静默失败,返回 false(严格模式可能报错)
console.log(globalVar);     // 100 —— 依然存在,删除无效

// ❌ 无法删除 configurable: false 的属性
var obj = {};
Object.defineProperty(obj, "fixed", {
  value: 42,
  configurable: false  // 不可配置 → 不可删除
});
delete obj.fixed; // 非严格模式:静默失败;严格模式:"use strict" 下抛出 TypeError

💡 代码解析:

  • var 声明的变量虽然挂在 window 上,但其属性描述符 configurablefalse,所以无法被 delete 删除
  • configurable: false 意味着属性不可被重新定义也不可被删除——Object.defineProperty 可精确控制属性行为

3.5 存在性判断:in 运算符

var user = {
  name: "Charlie",
  age: 32,
  email: "charlie@example.com"
};

// 基本用法:'属性名字符串' in 对象 → 返回布尔值
console.log("name" in user);    // true  —— user 有 name 属性
console.log("phone" in user);   // false —— user 没有 phone 属性

// 属性名来自变量时,变量会被求值为字符串后再检测
var field = "email";
console.log(field in user);     // true —— 等价于 "email" in user

// ⚠️ 重要:in 运算符会检查整条原型链!
// toString 是 Object.prototype 上的方法,user 通过原型链继承了它
console.log("toString" in user); // true —— 来自 Object.prototype,不是 user 自身的属性

// 只检查"自有属性"(不包括继承的):用 hasOwnProperty
console.log(user.hasOwnProperty("name"));     // true  —— name 是 user 自身的属性
console.log(user.hasOwnProperty("toString")); // false —— toString 是继承的,不是自身的

// 更安全的写法:通过 Object.prototype 借用,防止对象覆盖了自身的 hasOwnProperty
console.log(Object.prototype.hasOwnProperty.call(user, "name")); // true
// 等价的现代写法(ES2022)
// console.log(Object.hasOwn(user, "name")); // true

💡 代码解析:

表达式 检查范围 典型用途
"key" in obj 自身 + 原型链 判断某功能/接口是否可用(包括继承的)
obj.hasOwnProperty("key") 仅自身属性 遍历时过滤继承属性
Object.hasOwn(obj, "key") 仅自身属性(ES2022) 现代推荐写法,更安全
  • "phone" in userfalseuser 对象和它的整个原型链上都没有 phone 属性
  • "toString" in usertruetoString 定义在 Object.prototype 上,所有普通对象都能通过原型链访问到它

3.6 「数组 / 函数也是对象」的意义与坑点

// ① 数组也是对象 → 可以挂自定义属性
var list = [100, 200, 300, 400, 500]; // 创建数组,数字索引 0~4 是"特殊属性"
list.label = "scores";        // 像普通对象一样挂自定义属性
list.updatedAt = "2024-01-01";

console.log(list.label);     // "scores" —— 自定义属性正常读取
console.log(list.length);    // 5 —— length 只统计数字索引元素,自定义属性不计入

// ⚠️ 坑点:for...in 遍历数组时,会把自定义属性名也遍历出来
for (var k in list) {
  console.log(k); // "0" "1" "2" "3" "4" "label" "updatedAt" —— 数字索引 + 自定义属性混在一起
}

// delete 自定义属性:正常删除,属性消失
delete list.label;
// delete 数字索引:留下空洞,length 不变(陷阱!)
delete list[2];
console.log(list); // [100, 200, empty, 400, 500] —— 索引2变成空洞

console.log("label" in list);     // false —— 已被删除
console.log("length" in list);    // true  —— length 是数组固有属性
console.log("updatedAt" in list); // true  —— 自定义属性还在

💡 代码解析(数组是对象):

  • list.label = "scores" — 数组底层是对象,可以像对象一样挂任何属性,但这不是数组的正规用法
  • list.length = 5length 只统计数字索引0, 1, 2…)的个数,自定义属性名不计入
  • for...in 的坑:会遍历所有可枚举属性(包括自定义属性名和数字索引),因此遍历数组绝对不推荐用 for...in,应使用 for 循环、for...offorEach
// ② 函数也是对象 → 可以挂属性(常用于存储元数据)
function processData() {
  console.log("processing...");
}

// 函数也是对象,可以挂属性(这些属性存在函数对象本身上,与函数执行无关)
processData.version  = "2.1.0";  // 版本号元数据
processData.author   = "dev-team"; // 作者信息
processData.maxItems = 1000;      // 配置参数

console.log(processData.version); // "2.1.0" —— 直接读取函数对象的属性

// for...in 遍历函数挂载的属性(函数体内部代码不会被遍历)
for (var prop in processData) {
  console.log(prop, processData[prop]);
  // version   2.1.0
  // author    dev-team
  // maxItems  1000
}

console.log("version" in processData);  // true  —— version 属性存在
console.log("getInfo" in processData);  // false —— 没有 getInfo 属性

💡 代码解析(函数是对象):

  • processData.version = "2.1.0" — 函数是 Function 的实例,也是对象,可以挂属性
  • 挂在函数上的属性与函数内部的局部变量完全独立,互不影响
  • 工程应用:jQuery 插件用 $.fn.pluginName 挂载,Mocha/Jest 测试用例注解属性,Express 路由中间件挂 metadata

实际应用场景:函数挂属性常见于插件系统的版本标记、路由中间件的元数据、测试框架的用例标注等。


🏢 业务价值 · Object 基础操作

业务场景 涉及技术 价值说明
接口数据归一化 对象读写、属性遍历 前端对接后端 JSON 时,将不同格式的字段统一为内部模型
表单验证规则配置 对象字面量 { required: true, maxLength: 50 } 一对象即一套规则,便于复用
权限配置对象 in / hasOwnProperty 运行时判断菜单项是否在当前用户权限表内
删除敏感字段 delete 日志上报前去除用户 passwordtoken 等私密属性
插件元数据挂载 函数挂属性 Webpack Loader、Babel Plugin 在函数/类上挂版本号和配置项

4. 构造函数:类型判断、实例化、自定义与返回值

4.1 什么是构造函数

构造函数(Constructor)

系统内置构造函数

自定义构造函数

Object

Array

Function

Date

RegExp

Error

Map / Set / WeakMap...

function User(...) { this.x = ... }

function Product(...) { ... }

构造函数对应数据类型:每种内置类型都有其构造函数,同一类型的所有实例共享同一个构造函数。

4.2 instanceofconstructor 速查

var arr  = [10, 20, 30, 40, 50]; // 数组:Array 的实例
var fn   = function() {};         // 函数:Function 的实例
var user = { username: "Alex", age: 28 }; // 普通对象:Object 的实例
var date = new Date();            // 日期:Date 的实例

// ===== instanceof:沿原型链检查 =====
// arr 的原型链:arr → Array.prototype → Object.prototype → null
console.log(arr  instanceof Array);    // true  —— Array.prototype 在 arr 的原型链上
console.log(arr  instanceof Object);   // true  —— Object.prototype 也在链上(Array 继承自 Object)
console.log(fn   instanceof Function); // true  —— Function.prototype 在 fn 的链上
console.log(fn   instanceof Object);   // true  —— Function.prototype 继承自 Object.prototype
console.log(date instanceof Date);     // true
console.log(date instanceof Object);   // true  —— 所有内置类型都最终继承 Object

console.log(user instanceof Object);   // true  —— 普通对象直接继承 Object.prototype
console.log(user instanceof Array);    // false —— Array.prototype 不在 user 的链上

console.log(arr instanceof Function);  // false —— arr 不是函数
console.log(arr instanceof Date);      // false —— arr 不是日期

// ===== constructor:追溯创建该对象的构造函数 =====
// constructor 属性存储在各自的 prototype 上,实例通过原型链访问
console.log(arr.constructor);           // ƒ Array() { [native code] }  —— 指向 Array 构造函数
console.log(fn.constructor);            // ƒ Function() { [native code] }
console.log(user.constructor);          // ƒ Object() { [native code] }
console.log(date.constructor);          // ƒ Date() { [native code] }

// 同种类型的所有实例共享同一个构造函数(通过原型共享,不是每个实例独有)
console.log([].constructor === arr.constructor);        // true —— 两个数组的构造函数是同一个 Array
console.log(parseInt.constructor === fn.constructor);   // true —— parseInt 也是函数,构造函数都是 Function

💡 代码解析:

  • instanceof 的本质是原型链查找:检查右侧构造函数的 .prototype 是否出现在左侧对象的原型链上
  • arr instanceof Objecttrue 的原因:Array.prototype 本身也继承自 Object.prototype,所以数组"同时"是 Array 实例也是 Object 实例
  • constructor 属性来自原型,是可覆盖的(不可靠);更可靠的类型判断用 Array.isArray()Object.prototype.toString.call()
  • [].constructor === arr.constructortrue:两个数组实例的 constructor 都通过原型链指向同一个 Array 函数,不是各自独立的副本
instanceof 的内部逻辑(简化版)

arr instanceof Array

获取 arr 的 [[Prototype]]

等于 Array.prototype?

返回 true

继续沿原型链向上

到达 null?

返回 false

4.3 实例化:每次 new 都是新引用

// new 每次都在堆内存中分配一块新空间,创建独立对象
var arr1 = new Array(); // 第一次实例化:在内存地址 A 创建空数组
var arr2 = new Array(); // 第二次实例化:在内存地址 B 创建空数组(与 A 不同)
console.log(arr1 === arr2); // false —— === 比较的是引用地址,A ≠ B,所以为 false

// 字面量 [] 是 new Array() 的语法糖,行为完全一致
var x = [];   // 等价于 new Array(),分配新内存
var y = [];   // 等价于 new Array(),再分配新内存(与 x 不同)
console.log(x === y); // false —— 即使内容相同,引用不同就不相等

var obj1 = {};  // 等价于 new Object(),分配新内存
var obj2 = {};  // 再分配新内存
console.log(obj1 === obj2); // false

// 独立内存意味着修改一个不影响另一个
arr1.push(1, 2, 3);   // arr1 在自己的内存空间里存储 [1, 2, 3]
arr2.push(100, 200);  // arr2 在自己的内存空间里存储 [100, 200]
console.log(arr1);    // [1, 2, 3]   —— 各自独立
console.log(arr2);    // [100, 200]  —— 互不影响

💡 代码解析:

  • === 对对象(数组/普通对象/函数)的比较是引用比较,不是内容比较
  • 每次 new(或字面量)都会创建一个全新的对象,分配独立内存,因此两个变量即使存的是相同内容的对象,=== 仍为 false
  • 这与原始值不同:5 === 5true"hello" === "hello"true——原始值比较的是值本身

内存示意:

var x = [];  →  x 指向 内存地址 0x001 → []
var y = [];  →  y 指向 内存地址 0x002 → []
x === y ?   →  0x001 !== 0x002  →  false

4.4 自定义构造函数——批量生产对象的模具

// 定义构造函数:约定首字母大写(区别于普通函数,提示调用者要用 new)
function User(username, age, address) {
  // 用 new 调用时,this 自动指向新分配的空对象(后面会被返回为实例)
  this.name    = username; // 把参数值挂到 this(即新实例)的属性上
  this.age     = age;
  this.address = address;

  // 方法:每次 new 都会在实例上创建一个独立的函数副本(后续原型章节会优化为共享)
  this.addToCart = function(product) {
    // 此处的 this 在方法被调用时才确定,指向调用该方法的实例
    console.log(this.name + " added [" + product + "] to cart");
  };

  this.buy = function(product) {
    console.log(this.name + " purchased [" + product + "]");
  };
}

// new User(...) 的执行过程:
// 1. 创建空对象 {}
// 2. 将 this 绑定到该空对象
// 3. 执行函数体(this.name = username 等赋值)
// 4. 返回 this(即填充好属性的对象)
var u1 = new User("Alice", 28, "Downtown"); // u1 = { name:"Alice", age:28, address:"Downtown", ... }
var u2 = new User("Bob",   35, "Uptown");   // u2 = { name:"Bob",   age:35, address:"Uptown",   ... }

console.log(u1); // 查看 u1 的完整结构
console.log(u2); // 查看 u2 的完整结构
console.log(u1 === u2); // false —— 两次 new 产生的是两块独立内存,引用不同

// instanceof 验证:u1 的原型链上有 User.prototype
console.log(u1 instanceof User);   // true  —— u1 是 User 的实例
console.log(u1.constructor);       // User 函数 —— 通过原型链追溯到 User

// 调用方法:this 指向调用点左边的对象(u1 或 u2)
u1.addToCart("Mechanical Keyboard");
// 执行时 this = u1,所以 this.name = "Alice"
// 输出:Alice added [Mechanical Keyboard] to cart

u2.addToCart("Wireless Mouse");
// 执行时 this = u2,所以 this.name = "Bob"
// 输出:Bob added [Wireless Mouse] to cart

💡 代码解析:

  • 构造函数名首字母大写是约定,不是强制语法要求,但所有团队都遵守这个约定
  • this.name = usernamethisnew 调用时指向新建对象,每次 new 都是全新的 this
  • 方法定义在 this 上意味着每个实例都有独立的函数副本(u1 的 addToCart 和 u2 的 addToCart 是不同的函数对象),后面学习原型时会将方法移到 User.prototype 上,实现所有实例共享一个函数

内存示意图

new

new

[[Prototype]]

[[Prototype]]

u1 实例
{ name:'Alice', age:28, ... }

u2 实例
{ name:'Bob', age:35, ... }

User 构造函数

User.prototype

4.5 构造函数的 return 返回值规则

// 情形 ①:无 return(或 return 不带值)→ 返回新建实例(最常见的默认路径)
function Widget() {
  this.type  = "button"; // 给新实例挂属性
  this.color = "blue";
  // 没有 return,JS 引擎自动 return this(即新建的实例)
}
var w = new Widget();
console.log(w); // Widget { type: 'button', color: 'blue' } —— 就是 this 对象

// 情形 ②:return 原始类型(数字/字符串/布尔/null/undefined)→ 被忽略,仍返回新建实例
function Box() {
  this.label = "Package";
  return 42;  // ← return 了数字(原始类型),这行被 JS 引擎忽略!
}
var b = new Box();
console.log(b); // Box { label: 'Package' } —— 42 被丢弃,返回的还是实例

// 情形 ③:return 对象类型(数组/普通对象/函数/Date 等)→ 替换默认实例!
function Product() {
  console.log("Product start"); // 构造函数体照常执行
  return [10, 20, 30, 40, 50]; // ← return 了数组(对象类型),这个会被采用
}
var p = new Product();
console.log(p); // [10, 20, 30, 40, 50] —— 不是 Product 实例,而是 return 的那个数组!
// 注意:p instanceof Product 此时为 false

💡 代码解析:

情形 return 内容 new 的结果 记忆方法
无 / return; 新建实例(this) 默认行为,最常用
原始值(42, “str”, true) 新建实例(this) 原始值被忽略
对象类型([], {}, function…) return 的那个对象 对象会"劫持"结果
  • 规律:只有 return对象类型时,new 表达式的结果才不是 this
  • 实际工程中情形①是正常写法;情形③通常出现在高级设计模式(如工厂函数)中,初学阶段先掌握默认路径

记忆规则

是(数组/函数/普通对象)

否(number/string/boolean/undefined)

构造函数 return

return 的是对象类型?

new 表达式结果 = return 的对象

new 表达式结果 = 新建实例

4.6 构造函数 vs 普通函数调用

function User() {
  // 函数体内打印 this,结果取决于调用方式
  console.log("User called, this =", this);
}

// 用 new 调用 → this = 新建的空实例
var instance = new User();  // 打印:User called, this = User {}
console.log(instance);      // User {} —— 空实例(函数体内没有给 this 挂属性)

// 普通调用 → this = window(非严格模式)
var result = User();        // 打印:User called, this = Window {...}
console.log(result);        // undefined —— 普通调用时函数没有 return,默认返回 undefined

💡 代码解析:

  • 同一个函数,调用方式不同,行为截然不同:
    • new User()this = 新建实例,返回该实例
    • User()this = window,返回 undefined
  • instanceUser {} 而不是 undefined,因为 new 保证了返回值是新实例(哪怕函数体什么都没写)
  • 这就是为什么构造函数必须配合 new 使用,漏写 new 会导致 this 污染全局
// 有趣的边界情形:return 函数时,调用和实例化结果形态相同
function factory() {
  // 函数体 return 了一个函数(对象类型)
  return function() { console.log("factory output"); };
}

var r1 = factory();      // 普通调用:直接返回 return 的函数
var r2 = new factory();  // new 调用:return 了对象类型(函数),新实例被替换,r2 也是这个函数

console.log(typeof r1); // "function" —— 普通调用返回的函数
console.log(typeof r2); // "function" —— new 调用但因 return 函数,结果相同
// 结论:这种情况下,加不加 new 结果一样(不常见,仅作理解 return 机制用)

💡 代码解析:

  • factory 返回一个函数(函数是对象类型)
  • new factory() 时,本该返回新实例,但由于 return 了对象类型,新实例被"替换"掉
  • 这与之前 4.5 的"return 对象类型 → 替换实例"规则一致

工程实践:构造函数应始终与 new 配合使用。若需要强制保护,可在函数体加 if (!(this instanceof User)) return new User(...args) 检测。

4.7 对象数组排序(sort + 比较函数)

var products = [
  { name: "Keyboard", price: 89,  rating: 4.5 },
  { name: "Mouse",    price: 29,  rating: 4.8 },
  { name: "Monitor",  price: 320, rating: 4.3 },
  { name: "Headset",  price: 75,  rating: 4.6 }
];

// 按价格升序:比较函数返回 a.price - b.price
// 规则:返回负数 → a 排 b 前;返回正数 → b 排 a 前;返回 0 → 顺序不变
products.sort(function(a, b) {
  return a.price - b.price; // a 便宜 → 负数 → a 在前(升序)
});
console.log(products.map(p => p.name + "($" + p.price + ")"));
// ["Mouse($29)", "Headset($75)", "Keyboard($89)", "Monitor($320)"]

// 按评分降序:交换 a/b 顺序即可变升为降
products.sort(function(a, b) {
  return b.rating - a.rating; // b 评分高 → 正数 → b 排 a 前(降序)
});
console.log(products.map(p => p.name + "(" + p.rating + "⭐)"));
// ["Mouse(4.8⭐)", "Headset(4.6⭐)", "Keyboard(4.5⭐)", "Monitor(4.3⭐)"]

💡 代码解析:

  • Array.prototype.sort(compareFn) 的比较函数签名:function(a, b)
    • 返回负数a 排在 b 前(不交换)
    • 返回正数b 排在 a 前(交换)
    • 返回 0 → 顺序不变(稳定排序)
  • 升序公式return a.field - b.field(数值字段)
  • 降序公式return b.field - a.field(把 a/b 对调)
  • 注意:sort 原地修改数组,若需要保留原数组顺序,应先 .slice() 复制再排序

经典使用场景:电商平台「价格排序」「好评排序」;仪表盘数据表格排序;任务管理系统按优先级排列。


🏢 业务价值 · 构造函数与实例化

业务场景 涉及技术 价值说明
用户账号对象工厂 自定义构造函数 new User(data) 批量生成用户实例,统一挂 avatarpermissionstoken 等属性
商品/订单建模 构造函数 + 排序 从 API 拿到 JSON 数组后用 Product 构造函数标准化,再 sort 展示
多环境配置隔离 instanceof 判断当前配置是 DevConfig 实例还是 ProdConfig 实例,走不同分支
SDK 客户端 构造函数 + return 规则 new ApiClient({ baseURL }) 生成独立实例,客户端与服务端各自持有独立状态
列表渲染引擎 对象数组 + 比较函数 表格按多字段组合排序(先按状态分组、再按时间降序)

5. this:三大场景、宿主丢失、window 联动

5.1 this 的核心心法

不看书写位置,看调用方式。

this 写在哪里不重要

谁在调用这个函数?用什么方式?

直接调用 fn() → globalThis / undefined(严格)

方法调用 obj.fn() → obj

new 调用 new Fn() → 新实例

显式绑定 fn.call(ctx) → ctx

5.2 场景 ① 全局脚本顶层与普通调用

// 全局脚本顶层(不在任何函数内):this = window(浏览器,非严格模式)
console.log(this);            // Window {...} —— 就是浏览器的全局对象
console.log(this === window); // true —— 顶层 this 和 window 是同一个对象

// 普通函数调用(没有 obj. 前缀,没有 new):this 也指向 window
function showThis() {
  // "直接调用函数"——没有明确的调用者,非严格模式默认绑定到全局对象
  console.log(this);            // Window —— 同顶层的 this
  console.log(this === window); // true
}
showThis(); // 普通调用,不是方法调用,也没有 new

// 严格模式下:普通调用的 this 变为 undefined(防止意外修改全局)
function strictFn() {
  "use strict"; // 在函数内开启严格模式(ES5+)
  // 严格模式下,函数被普通调用时 this 不再默认绑定全局,而是 undefined
  console.log(this); // undefined
}
strictFn(); // 普通调用,严格模式,this = undefined

💡 代码解析:

  • 顶层 this:在浏览器的非模块脚本中,全局作用域下 this === window
  • 普通调用fn()):非严格模式下 this 自动绑定到全局对象 window;严格模式下 thisundefined
  • "use strict" 的重要性:开启严格模式后,普通调用不再隐式绑定全局,有助于尽早发现 this 指向错误的 bug(否则意外修改了 window 属性,很难排查)

5.3 场景 ② 构造函数内部

function Device(brand, model) {
  // 用 new 调用时,this 是引擎为我们新创建的空对象
  // 以下两行把参数值存入新对象的属性
  this.brand = brand; // 相当于:新实例.brand = brand
  this.model = model;

  this.describe = function() {
    // 此处 this 在方法被调用时才确定,指向调用者(调用 describe 的那个实例)
    return this.brand + " " + this.model;
  };
  // 函数体执行完,引擎隐式 return this(新建的实例)
}

// 每次 new 都创建一个全新的实例,this 指向各自的新对象
var phone  = new Device("Apple", "iPhone 16"); // phone.brand = "Apple", phone.model = "iPhone 16"
var laptop = new Device("Dell",  "XPS 15");    // laptop.brand = "Dell", laptop.model = "XPS 15"

console.log(phone.describe());  // "Apple iPhone 16" —— this = phone
console.log(laptop.describe()); // "Dell XPS 15"     —— this = laptop

// 用一个简单的例子验证:new 时 this 就是新建的那个对象
function ShowSelf() {
  // this 是新建的空对象,此时打印出来是 ShowSelf {}
  console.log("this in constructor:", this);
}
var inst = new ShowSelf(); // 打印 ShowSelf {} —— 证明 this 是新实例

💡 代码解析:

  • new Device(...) 执行时,函数体内的 this 被绑定到正在创建的那个新对象
  • this.brand = brand — 把外部传入的参数值存到新对象上,这是构造函数初始化实例的核心操作
  • describe 方法里的 this 并不是在定义时固定的,而是在被调用时才决定:phone.describe()this = phonelaptop.describe()this = laptop

5.4 场景 ③ 方法调用:「看点号左侧」

var user = {
  name: "Alice",
  age: 28,
  getInfo: function() {
    // 此函数被 user.getInfo() 调用,点号左边是 user,所以 this = user
    console.log("getInfo this:", this);  // this 就是 user 对象
    console.log("name:", this.name);     // user.name = "Alice"
  },
  friend: {
    name: "Bob",
    say: function() {
      // 此函数被 user.friend.say() 调用,点号链的最右端的直接调用者是 user.friend
      // 所以 this = user.friend,而不是 user!
      console.log("say this:", this);    // this 是 user.friend 对象
      console.log("name:", this.name);   // user.friend.name = "Bob"
    }
  }
};

// 调用 getInfo:点号左边是 user → this = user
user.getInfo();
// this → user 对象({ name:"Alice", age:28, ... })
// name: Alice

// 调用 say:点号链 user.friend.say(),直接调用者是 user.friend → this = user.friend
user.friend.say();
// this → user.friend 对象({ name:"Bob", ... }),不是 user!
// name: Bob

💡 代码解析:

  • 黄金规则obj.fn() 调用时,this = obj(点号左侧的那个对象)
  • user.friend.say() — 链式调用时,看紧挨着方法名左边的那个对象,即 user.friend,不是最左边的 user
  • 简单记法:谁直接调用了这个函数,this 就是谁("直接"意味着就是点号左边的那一级)

嵌套对象中的 this

user.friend.say()

点号链最右侧的调用者是 user.friend

this = user.friend

this.name = 'Bob'

5.5 宿主丢失:赋值出来的函数

var globalName = "GlobalScope";
var obj = {
  name: "ObjectScope",
  greet: function() {
    console.log(this.name); // this 在调用时决定
  }
};

// ✅ 正常方法调用:obj.greet() → 点号左边是 obj → this = obj
obj.greet(); // "ObjectScope" —— this.name = obj.name = "ObjectScope"

// ❌ 赋值给变量后调用:宿主丢失!
// detached 只是"搬走了"函数的引用,与 obj 之间没有绑定关系
var detached = obj.greet; // 把函数引用赋给 detached,不是调用
var name = "GlobalScope"; // 全局变量 name(挂在 window 上)
// detached() 是直接调用,没有点号前缀,this 变为 window
detached(); // "GlobalScope" —— this = window,this.name = window.name = "GlobalScope"

// ❌ 作为回调传递 → 在 runner 内部被普通调用 fn(),同样丢失宿主
function runner(fn) {
  fn(); // fn 是普通调用,没有调用者,this = window
}
runner(obj.greet); // "GlobalScope" —— 函数引用传进去,宿主 obj 没有传进去

// ✅ 解决方案:bind 永久绑定 this
// bind 返回一个新函数,无论怎么调用,this 始终是第一个参数(obj)
var boundGreet = obj.greet.bind(obj); // 创建绑定了 obj 的新函数
boundGreet();          // "ObjectScope" —— this 被固定为 obj
runner(boundGreet);    // "ObjectScope" —— 即使在 runner 内普通调用,this 仍是 obj

💡 代码解析:

  • var detached = obj.greet — 这一行只是复制函数引用,等号左边的变量名和等号右边的 obj 对象之间没有绑定,this 信息随着 obj. 的脱落而丢失
  • detached() — 普通调用,this = window;而 obj.greet() — 方法调用,this = obj
  • 同一个函数,调用方式不同,this 就不同,这是 JS 中最容易混淆的特性
  • bind(obj) — 返回一个新函数,内部永久绑定 this = obj,无论如何调用都不会丢失

工程中最常见的宿主丢失场景

// ① DOM 事件回调
var timer = {
  count: 0,
  start: function() {
    // 错误:setTimeout 内部调用,this → window
    setTimeout(function() {
      this.count++; // window.count,不是 timer.count
    }, 1000);

    // 正确①:bind
    setTimeout(function() {
      this.count++;
    }.bind(this), 1000);

    // 正确②:箭头函数(继承外层 this)
    setTimeout(() => {
      this.count++; // 箭头函数内 this 是 timer
    }, 1000);
  }
};

5.6 全局变量与 window 属性的联动

// var 声明的全局变量等于 window 的属性
var pageTitle = "Dashboard";
console.log(window.pageTitle === pageTitle); // true

// 普通函数调用时,this === window
var counter = 0;
function increment() {
  this.counter = this.counter + 1; // 等价于 window.counter = window.counter + 1
  console.log(counter);            // 读的是全局 counter(已被修改)
}
increment();
console.log(counter); // 1
// 经典面试题:全局属性改写(深度理解 this 与全局变量的关联)
var foo = 123; // ① 全局变量 foo = window.foo = 123(var 声明的变量挂在 window 上)

function print() {
  // ② print() 是普通调用,非严格模式下 this = window
  this.foo = 234; // 等价于 window.foo = 234,直接修改了全局变量 foo!

  // ③ console.log(foo) 用标识符查找:
  //    先找当前函数作用域 → 没有局部 foo
  //    再找全局作用域 → 找到 window.foo,此时已被 ② 改为 234
  console.log(foo); // 234(不是 123!因为 ② 已经修改了全局 foo)
}
print();
// 执行完 print 后,全局 foo 已被改为 234
console.log(foo); // 234 —— 全局变量已被污染

💡 代码解析:

  • var foo = 123 — 全局 var 声明,等价于 window.foo = 123
  • this.foo = 234(在 print() 普通调用中)— thiswindow,所以等价于 window.foo = 234直接覆盖了全局变量 foo
  • console.log(foo) — 标识符 foo 查找规则:函数作用域内无局部 foo,向上找到全局 foo,此时已是 234
  • 输出 234 → 234 的两个关键点:①this.foo = 234 改写了全局 foo;②后面两处读到的都是被改写后的值

5.7 new 与普通调用 + 变量提升的组合题

var a = 5; // 全局变量 a = 5,挂在 window 上(window.a = 5)

function test() {
  // ⚠️ 关键:因为函数内有 var a,所以 var 提升到函数顶部
  // 提升后的等价代码:
  // var a;      ← 声明被提升,值为 undefined(遮蔽了全局 a)
  // a = 0;      ← 给局部 a 赋值为 0
  // console.log(a);      0
  // console.log(this.a); 取决于 this
  // var a; (提升后这里已无意义)
  // console.log(a);      0

  a = 0;           // 给局部变量 a 赋值为 0(不影响全局 a!)
  console.log(a);  // ① 0(读的是局部变量 a = 0)
  console.log(this.a); // ② 取决于调用方式(见下方分析)
  var a;           // 声明已被提升,这行运行时等同于空语句
  console.log(a);  // ③ 0(仍是局部变量 a)
}

test();
// 普通调用:this = window
// ① 0          —— 局部 a = 0
// ② 5          —— this = window,window.a = 全局 a = 5(局部 a 没有改变全局!)
// ③ 0          —— 局部 a 仍是 0

console.log("");

new test();
// new 调用:this = 新建的空实例对象(没有任何自有属性)
// ① 0          —— 局部 a = 0(同上)
// ② undefined  —— this 是新实例,新实例上没有属性 a,读不到,返回 undefined
// ③ 0          —— 局部 a 仍是 0

💡 代码解析(三个关键点):

  1. 变量提升(Hoisting)var a 在函数体任何位置声明,都会被提升到函数顶部,导致函数内的 a = 0 赋给的是局部变量,而不是全局变量 a

  2. 局部变量遮蔽:局部 a 存在后,函数体内的 a = 0 / console.log(a) 操作的都是局部变量,全局 a 的值 5 始终未被修改

  3. this.a vs a 的区别

    • a(标识符)— 沿作用域链查找:先找局部→再找全局
    • this.a(属性访问)— 在 this 指向的对象上查找:test()this=window,读 window.a=5new test()this 是空实例,无属性 a,得 undefined

分析流程

var a 提升到 test 顶部

局部变量 a 遮蔽全局 a

a = 0 → 赋给局部 a

console.log(a) → 0 (局部)

console.log(this.a)

test() 调用: this=window, window.a=5 → 5

new test(): this=新实例, 无属性a → undefined


6. 包装对象与原始类型双形态

6.1 原始类型的三种存在形态

// 形态 ① 直接量(原始值):最常用,存储的是值本身,不是对象
var n1 = 89;

// 形态 ② Number() 函数调用(不加 new):把参数转为数字原始值
var n2 = Number(88); // 作为普通函数调用,返回原始值 88,不是对象

// 形态 ③ new Number() 构造函数:创建 Number 包装对象(很少用)
var n3 = new Number(87); // 加了 new,返回的是对象,而非原始值

// typeof 揭示本质区别
console.log(typeof n1); // "number"  —— 原始值
console.log(typeof n2); // "number"  —— 原始值(Number() 函数调用返回原始值)
console.log(typeof n3); // "object"  —— ← 注意!包装对象的 typeof 是 "object"

// instanceof 只对对象有效
console.log(n1 instanceof Number); // false —— n1 是原始值,不是对象,instanceof 永远 false
console.log(n2 instanceof Number); // false —— 同理
console.log(n3 instanceof Number); // true  —— n3 是 Number 包装对象,是 Number 的实例

// 算术运算时,包装对象自动拆箱(unboxing)为原始值参与计算
console.log(n1 * n3); // 89 × 87 = 7743 —— n3 自动转为 87 参与乘法

// 访问 constructor 属性:原始值会临时包装成对象,然后访问原型链上的 constructor
console.log(n1.constructor); // Number —— 临时创建 new Number(89) → 访问 constructor → 销毁临时对象
console.log(n3.constructor); // Number —— n3 本身就是对象,直接访问

💡 代码解析:

创建方式 示例 typeof instanceof Number 是否对象
直接量 var n = 89 "number" false
Number() var n = Number(88) "number" false
new Number() var n = new Number(87) "object" true
  • typeof n3"object" 是一个陷阱:用 typeof 区分 Number 原始值和包装对象时要注意这一点
  • n1.constructor 能访问成功,是因为引擎在访问属性那一刻临时创建了 new Number(89),访问完立即销毁——这就是"临时包装"

6.2 String、Boolean 的对象形态

// 字符串直接量:访问属性时引擎临时创建 new String("hello") 包装对象
console.log("hello".constructor);   // String   —— 包装对象的构造函数
console.log("hello".length);        // 5        —— 包装对象的 length 属性
console.log("hello".toUpperCase()); // "HELLO"  —— 调用包装对象的方法

// 布尔直接量:访问属性时引擎临时创建 new Boolean(...) 包装对象
console.log(true.constructor);  // Boolean —— 布尔包装对象的构造函数
console.log(false.constructor); // Boolean

// 数字直接量的特殊写法(注意点的歧义)
console.log(12.2.constructor);  // Number —— 12.2 是数字,第二个点是属性访问
console.log((12).constructor);  // Number —— 用括号消歧义
console.log(12..constructor);   // Number —— 两个点:第一个是小数点,第二个是属性访问

💡 代码解析:

  • 字符串"hello" 是原始类型,本身没有属性,但访问 .length.toUpperCase() 时,引擎在幕后临时创建 new String("hello"),访问完立刻丢弃
  • 布尔值:同理,true.constructor 触发临时 new Boolean(true) 包装,读取 constructor 后销毁
  • 数字直接量的点语法12.constructor 会报语法错误(引擎把 . 当小数点),正确写法是 (12).constructor12..constructor(第一个点是小数,第二个点是属性访问)

6.3 「临时包装」导致的经典陷阱

// 数组是对象 → 属性可以持久挂载
var arr = [];
arr.address = "Metro City"; // 挂在数组对象上,持久保存
console.log(arr.address);   // "Metro City" ← 成功,数组是真正的对象

// 原始字符串 → 临时包装对象,属性挂了也白挂
var msg = "";           // 原始字符串,不是对象
msg.rating = 5;         // ① 引擎临时创建 new String("") ② 给临时对象赋 rating=5 ③ 语句结束立即销毁
console.log(msg.rating); // undefined ← 再次访问 msg.rating 时,重新创建新临时对象,新对象没有 rating

💡 代码解析:

  • arr 是数组(对象类型),arr.address 像普通对象属性一样持久存在
  • msg 是原始字符串(非对象),msg.rating = 5 虽然没有报错,但:
    1. 引擎临时生成一个 new String("") 包装对象
    2. rating = 5 赋给这个临时对象
    3. 赋值语句结束 → 临时对象立即销毁
    4. 下次读 msg.rating → 引擎再生成一个全新的临时对象 → 新对象没有 rating → 返回 undefined
  • 记忆口诀:原始类型挂属性 = 往冰块上刻字,融化就消失

原因分析

临时包装对象 JS 引擎 代码 临时包装对象 JS 引擎 代码 新对象没有 rating! msg.rating = 5 创建 new String(msg) Temp.rating = 5 语句结束,销毁 Temp console.log(msg.rating) 创建新的 new String(msg) 读取 Temp.rating 返回 undefined

6.4 何时需要包装对象?

场景 建议
普通数值运算、字符串处理 用直接量,引擎自动临时包装
需要判断是否为 Number 类型实例 typeofinstanceof 注意区别
需要给字符串挂持久属性 改用对象 { value: "...", extra: ... }
面试题「为何 str.length 能用但属性挂不住」 临时包装机制

🏢 业务价值 · this 与包装对象

业务场景 涉及技术 价值说明
React 类组件事件绑定 bind(this) / 箭头函数 onClick={this.handleClick.bind(this)} 是最常见的 this 修复,也是箭头函数类属性写法的来源
Vue 选项式 API 方法 方法调用中的 this Vue 内部将 methods 中的函数以 obj.method() 形式调用,this 自动指向组件实例
setTimeout / setInterval 回调 宿主丢失 → 箭头函数 定时器回调中的 this 丢失是高频 Bug,箭头函数是最简修复
字符串/数字 API 调用 临时包装 " trim me ".trim()(42).toString(16) 背后都是临时包装对象提供的方法
严格模式下的 this "use strict" 生产构建工具(Webpack/Vite/esbuild)默认开启严格模式,普通调用 this 为 undefined,避免全局污染

7. 综合案例:对象数组排序与回调函数

7.1 商品列表排序(实战还原)

var inventory = [
  { id: 1, name: "Wireless Keyboard", price: 89,  stock: 150, rating: 4.5 },
  { id: 2, name: "USB-C Hub",         price: 45,  stock: 80,  rating: 4.7 },
  { id: 3, name: "4K Monitor",        price: 399, stock: 30,  rating: 4.2 },
  { id: 4, name: "Ergonomic Mouse",   price: 65,  stock: 200, rating: 4.8 },
  { id: 5, name: "Webcam HD",         price: 55,  stock: 90,  rating: 4.4 }
];

// 四个具名比较函数——命名清晰,可复用,可单独测试
function sortByPriceAsc(a, b)   { return a.price  - b.price; }   // 升序:差为负 → a 在前
function sortByPriceDesc(a, b)  { return b.price  - a.price; }   // 降序:交换 a/b
function sortByRatingDesc(a, b) { return b.rating - a.rating; }  // 评分高的排前面
function sortByStockAsc(a, b)   { return a.stock  - b.stock; }   // 库存少的排前面

// .slice() 先复制原数组,避免 .sort() 原地修改 inventory
var byPrice = inventory.slice().sort(sortByPriceAsc);
console.log("按价格升序:", byPrice.map(i => i.name + "($" + i.price + ")"));
// ["USB-C Hub($45)", "Webcam HD($55)", "Ergonomic Mouse($65)", "Wireless Keyboard($89)", "4K Monitor($399)"]

var byRating = inventory.slice().sort(sortByRatingDesc);
console.log("按评分降序:", byRating.map(i => i.name + "(" + i.rating + "★)"));
// ["Ergonomic Mouse(4.8★)", "USB-C Hub(4.7★)", "Wireless Keyboard(4.5★)", "Webcam HD(4.4★)", "4K Monitor(4.2★)"]

💡 代码解析:

  • 具名比较函数:将比较逻辑提取为独立的具名函数,代码更清晰,也方便单元测试
  • inventory.slice()Array.prototype.sort原地排序,会修改原数组。先 .slice() 复制一份,保留原始 inventory 不变,每次排序都基于原始数据
  • 回调函数模式sort(sortByPriceAsc) 中,sortByPriceAsc 是回调函数,传入函数引用(不加括号),sort 在内部按需调用它
  • 实战延伸:若字段是字符串(如 name),应用 localeComparereturn a.name.localeCompare(b.name)

7.2 回调函数与返回值的经典陷阱

var compute = function() {
  return 200;
};
// ⚠️ 关键区别:函数引用 vs 函数调用
console.log(compute);   // ƒ () { return 200; } —— 打印函数本身(没有括号,不调用)
console.log(compute()); // 200 —— 调用函数,打印返回值

// 陷阱一:回调函数有返回值,但外层函数没有把返回值传出去
function run(a, b, callback) {
  callback(a, b);       // ← 调用了 callback,但没有 return!
                        //   callback 的返回值被丢弃了
}

function sum(x, y) {
  return x + y; // 内部确实 return 了 300
}

console.log(run(100, 200, sum)); // undefined —— run 没有 return,所以结果是 undefined

// 修正版:外层函数也 return 回调的结果
function runAndReturn(a, b, callback) {
  return callback(a, b); // ← 加了 return,把 callback 的返回值传出去
}
console.log(runAndReturn(100, 200, sum)); // 300 —— 正确

💡 代码解析:

  • compute vs compute():变量名后面有没有 () 是天壤之别——无括号是引用(函数对象),有括号是调用(执行并拿返回值)
  • 回调返回值陷阱run 函数内 callback(a, b) 确实执行了 sumsumreturn 300,但 run 没有把这个 300 传递出去(没有 return callback(...)),导致 run 自己返回 undefined
  • 修复方式:在外层函数中也加 return callback(a, b),形成"透传"
  • 实际场景:Express 中间件 next() 返回值不向上传递就是同理;Promise 链中忘记 return 导致链断裂也是相同原理

7.3 递归函数的括号陷阱

// ❌ 错误版:if 后没有花括号,导致 return 游离在 if 作用域之外
function funWrong(n) {
  if (n <= 0)
    console.log(0);  // 只有这一行属于 if 块
    return;          // ← 这行不属于 if!永远执行,函数一进来就 return
  console.log(n);    // ← 永远不会执行到这里
  funWrong(n - 1);
  console.log(n);
}
// funWrong(3) 只会输出 0(n=3>0,if不执行console.log(0)),然后立即 return,后续全部跳过

// ✅ 正确版:用花括号明确 if 语句块的边界
function fun(n) {
  if (n <= 0) {   // 花括号包裹 if 块
    console.log(0);
    return;       // 只有 n <= 0 时才 return,终止递归
  }
  console.log(n); // n > 0 时,先打印 n
  fun(n - 1);     // 递归调用,深入下一层
  console.log(n); // 从递归返回后,再次打印 n(回溯阶段)
}
fun(3); // 输出顺序:3 → 2 → 1 → 0 → 1 → 2 → 3

💡 代码解析:

  • 错误版核心问题:Python 用缩进决定代码块,JavaScript 用花括号。没有花括号时,if 只包含紧随其后的一行,return 看起来属于 if 但实际上不是,导致函数每次调用都立刻 return
  • 正确版执行流程
    • 进栈阶段(n=3,2,1):每层先 console.log(n) → 再调 fun(n-1)
    • 终止条件(n=0):打印 0 并 return
    • 出栈阶段(n=1,2,3):每层从递归调用返回后,再执行 console.log(n)
  • 递归两要素:① 终止条件(n<=0 时 return)② 递归调用(fun(n-1)),缺一不可

递归调用栈示意

fun(0) fun(1) fun(2) fun(3) 主调用 fun(0) fun(1) fun(2) fun(3) 主调用 调用 fun(3) 输出 3 调用 fun(2) 输出 2 调用 fun(1) 输出 1 调用 fun(0) 输出 0,return 返回 输出 1 返回 输出 2 返回 输出 3 返回

8. 面试题精析:全覆盖解读

8.1 题目一:方法被搬运后的 this

var name = "222";
var a = {
  name: "111",
  say: function() {
    console.log(this.name);
  }
};

var fun = a.say;
fun();      // ① 输出?
a.say();    // ② 输出?

var b = {
  name: "333",
  say: function(func) {
    func(); // 普通调用
  }
};
b.say(a.say);  // ③ 输出?
b.say = a.say;
b.say();       // ④ 输出?

逐步分析

① fun() → 普通调用
this = window
window.name = '222'
输出 222

② a.say() → 方法调用
this = a
a.name = '111'
输出 111

③ b.say(a.say)
→ b.say 内部 func() 普通调用
this = window → 输出 222

④ b.say() → 方法调用
this = b
b.name = '333'
输出 333

答案:222111222333

💡 代码解析:

  • fun()fun = a.say 只是把函数引用赋给变量,fun() 是普通调用(没有 obj.),this = windowwindow.name = "222" → 输出 222
  • a.say():方法调用,点号左侧是 athis = aa.name = "111" → 输出 111
  • b.say(a.say)b.say 内部执行 func()func 是参数传入的 a.say 函数引用,func() 是普通调用,this = window → 输出 222(函数传递不带走 this 绑定!)
  • b.say():先把 a.say 赋给 b.say,再以方法调用 b.say(),点号左侧是 bthis = bb.name = "333" → 输出 333

8.2 题目二:全局属性改写

var foo = 123;       // 全局变量 foo(等价于 window.foo = 123)
function print() {
  this.foo = 234;    // 普通调用 this=window → 修改了 window.foo 为 234
  console.log(foo);  // 标识符 foo → 当前函数无局部 foo → 向上找全局 foo → 此时已被改为 234
}
print();             // this = window(非严格模式普通调用)
console.log(foo);    // 234(全局 foo 被 print() 的 this.foo=234 修改过)

两次输出都是 234。关键点:this.foo = 234 修改了全局变量 foo(因为 this === windowwindow.foo === foo)。

💡 代码解析:

  • var foo = 123 在全局作用域声明,等价于给 window 加了属性 window.foo = 123
  • print() 是普通调用,this = window,所以 this.foo = 234 实际上是 window.foo = 234
  • 函数内 console.log(foo) 中的 foo标识符查找(沿作用域链),print 内无局部 foo,向上找到全局 foo,此时已被改为 234
  • 函数返回后,全局 foo 已是 234,所以第二个 console.log(foo) 也是 234

8.3 题目三:new vs 调用 + 变量提升

var a = 5; // 全局变量 a = 5(window.a = 5)

function test() {
  // ⚠️ 变量提升:var a 被提升到函数顶部,相当于:var a; (初始值 undefined)
  // 因此函数内 a 是局部变量,与全局 a 无关
  a = 0;           // 局部 a 赋值为 0(不影响全局 a)
  console.log(a);  // 0(局部 a)
  console.log(this.a); // 取决于 this 指向(见下方分析)
  var a;           // var 声明被提升到顶部了,这里什么都没发生
  console.log(a);  // 0(局部 a 仍然是 0)
}

// 场景一:普通调用
test();
// this = window(非严格模式)
// window.a = 5(全局 a 没被动过)
// console.log(this.a) → window.a → 5
// 输出顺序:0  →  5  →  0

// 场景二:new 调用
new test();
// this = 新建的空实例(没有属性 a)
// console.log(this.a) → 实例没有 a → undefined
// 输出顺序:0  →  undefined  →  0

💡 代码解析:

  • 变量提升var atest 函数内部的任何位置声明,都会被提升到函数作用域顶部,所以整个函数内 a 都是局部变量,与全局 var a = 5 完全隔离
  • 普通调用(test()this = windowthis.a = window.a = 5(全局 a 未被局部变量影响)
  • new 调用(new test()this = 新建空实例,实例上没有任何属性,this.aundefined
  • 核心考点:变量提升 + this 指向 + 局部变量与全局变量的作用域隔离,三者叠加出现

8.4 题目四:嵌套引用与属性赋值

var age = 20; // 全局 age = 20

var obj = {
  age: 10,          // obj 自己的 age = 10
  getAge: function() {
    console.log(this.age); // this.age 取决于调用方式
  }
};

var obj1 = { age: 30 };  // obj1 自己的 age = 30
obj1.prop = obj;          // obj1.prop 指向 obj 同一个引用(不是复制!)

var fn = obj1.prop.getAge; // 把 getAge 函数引用赋给 fn(脱离了对象)

obj.getAge();           // ① 方法调用:this = obj,输出 obj.age = 10
obj1.prop.getAge();     // ② 方法调用:this = obj1.prop(即 obj),输出 obj.age = 10
fn();                   // ③ 普通调用:this = window,输出 window.age = 20

💡 代码解析:

  • obj.getAge():点号左侧是 objthis = objthis.age = obj.age = 10
  • obj1.prop.getAge():点号链中,this 绑定到紧挨着方法的那个对象,即 obj1.prop,而 obj1.prop === obj(引用同一对象),所以 this.age = obj.age = 10(不是 obj1.age = 30obj1 只是中间的"跳板")
  • fn():把方法赋给变量后普通调用,this = windowwindow.age = 全局 age = 20
  • 黄金法则this = 调用方法时,点号左边直接相邻的那个对象,链式 a.b.c()this = b,不是 a

注意obj1.prop.getAge() 中,this 是点号链最右侧方法的直接调用者 obj1.prop,而 obj1.prop === obj,所以 this.age === obj.age === 10

8.5 题目五:构造函数内 this 的使用场景

var age = 10; // 全局 age = 10(window.age = 10)

function func(name, age) {
  // 注意:参数 age 与全局 age 同名,在函数内部参数遮蔽全局
  this.name = name; // 把 name 参数挂到 this 上
  this.age  = age;  // 把参数 age 挂到 this 上
  this.getInfo = function() {
    console.log(this.name); // 方法调用时 this = 调用者
    console.log(this.age);
  };
  console.log(this.name); // 立刻打印 this.name
}

// 场景一:普通调用
func("Tom", 19);
// this = window,window.name = "Tom",window.age = 19(覆盖了全局 age!)
// console.log(this.name) → window.name → "Tom"

// 场景二:new 调用
var o = new func("Tom", 19);
// this = 新建实例 o,o.name = "Tom",o.age = 19
// console.log(this.name) → o.name → "Tom"

// 以方法调用 o.getInfo(),this = o
o.getInfo();
// this.name = o.name = "Tom"  →  打印 "Tom"
// this.age  = o.age  = 19    →  打印 19

// 最后检查全局 age
console.log(age);
// func("Tom", 19) 普通调用时,this=window,this.age=19 → window.age=19
// window.age 与全局变量 age 是同一个(var age 声明的全局变量挂在 window 上)
// 所以全局 age 已被改为 19,输出 19

💡 代码解析:

  • 普通调用污染全局func("Tom", 19)this = windowthis.age = 19 实际上是 window.age = 19覆盖了最初的全局 var age = 10
  • new 调用隔离new func("Tom", 19)this = 新实例 o,所有赋值都在实例上,与全局作用域隔离
  • 参数名与全局变量同名:函数参数 age 在函数作用域内遮蔽了全局 agethis.age = age 中右侧 age 取的是参数值
  • 结论:这道题综合考查了普通调用 vs new 调用、this 写属性对全局的副作用、以及方法调用 this 绑定

8.6 经典 this 场景速查表

调用形式 this 的值 示例
fn() window(非严格)/ undefined(严格) greet()
obj.fn() obj user.greet()
new Fn() 新建实例 new User()
fn.call(ctx) ctx greet.call(obj)
fn.apply(ctx) ctx greet.apply(obj)
fn.bind(ctx)() ctx(永久绑定) greet.bind(obj)()
箭头函数 () => {} 定义时外层 this(词法绑定) () => this.name
顶层脚本 window(非严格浏览器) console.log(this)

9. 可运行交互演示页(含本地图片)

以下是一个完整可运行的 HTML 演示页,涵盖本章所有核心知识点,并使用 images/demo-banner.svg 作为页面横幅图。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>JavaScript OOP Lab — Object / Constructor / this</title>
  <style>
    /* ===== CSS 自定义属性(设计令牌)===== */
    :root {
      --bg:       #0f172a;
      --surface:  #1e293b;
      --border:   rgba(148, 163, 184, 0.18);
      --text:     #e2e8f0;
      --muted:    #94a3b8;
      --accent:   #38bdf8;
      --accent2:  #818cf8;
      --green:    #4ade80;
      --red:      #f87171;
      --radius:   12px;
    }

    /* ===== 全局重置 ===== */
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    body {
      font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
      background: var(--bg);
      color: var(--text);
      line-height: 1.65;
      min-height: 100vh;
    }

    /* ===== 横幅 ===== */
    .hero-wrap {
      max-width: 1000px;
      margin: 0 auto;
      padding: 24px 20px 0;
    }
    .hero {
      border-radius: 16px;
      overflow: hidden;
      box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
    }
    .hero img {
      display: block;
      width: 100%;
      height: auto;
      object-fit: cover;
    }

    /* ===== 主内容 ===== */
    main {
      max-width: 1000px;
      margin: 0 auto;
      padding: 24px 20px 60px;
      display: grid;
      gap: 20px;
    }

    /* ===== 标签导航 ===== */
    .tabs {
      display: flex;
      gap: 8px;
      flex-wrap: wrap;
    }
    .tab-btn {
      cursor: pointer;
      border: 1px solid var(--border);
      border-radius: 8px;
      padding: 8px 16px;
      background: var(--surface);
      color: var(--muted);
      font-size: 14px;
      font-weight: 500;
      transition: all 0.2s ease;
    }
    .tab-btn:hover, .tab-btn.active {
      background: var(--accent);
      color: #0f172a;
      border-color: var(--accent);
    }

    /* ===== 卡片 ===== */
    .card {
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 20px 22px;
      display: none;
    }
    .card.active { display: block; }

    h2 {
      font-size: clamp(16px, 2.5vw, 20px);
      margin-bottom: 12px;
      color: var(--accent);
    }
    p { color: var(--muted); font-size: 14px; margin-bottom: 12px; }

    /* ===== 按钮 ===== */
    .run-btn {
      cursor: pointer;
      border: 0;
      border-radius: 8px;
      padding: 10px 18px;
      background: linear-gradient(135deg, #2563eb, #7c3aed);
      color: #fff;
      font-weight: 600;
      font-size: 14px;
      transition: transform 0.15s ease, filter 0.15s ease;
    }
    .run-btn:hover {
      transform: translateY(-2px);
      filter: brightness(1.1);
    }
    .clear-btn {
      cursor: pointer;
      border: 1px solid var(--border);
      border-radius: 8px;
      padding: 10px 14px;
      background: transparent;
      color: var(--muted);
      font-size: 13px;
      margin-left: 8px;
      transition: border-color 0.15s;
    }
    .clear-btn:hover { border-color: var(--accent); color: var(--accent); }

    /* ===== 输出框 ===== */
    .out {
      margin-top: 14px;
      padding: 14px 16px;
      background: #0b1220;
      border: 1px solid rgba(56, 189, 248, 0.2);
      border-radius: 10px;
      font-family: "Fira Code", "Cascadia Code", "Consolas", monospace;
      font-size: 13px;
      line-height: 1.7;
      white-space: pre;
      overflow-x: auto;
      min-height: 60px;
      color: var(--green);
    }

    /* ===== 代码高亮色 ===== */
    .out .kw  { color: var(--accent2); }
    .out .str { color: var(--green); }
    .out .num { color: var(--accent); }

    /* ===== 输入框 ===== */
    .input-row {
      display: flex;
      gap: 10px;
      align-items: center;
      flex-wrap: wrap;
      margin-bottom: 12px;
    }
    .input-row input {
      background: #0b1220;
      border: 1px solid var(--border);
      border-radius: 8px;
      padding: 8px 12px;
      color: var(--text);
      font-size: 14px;
      width: 160px;
    }
    label { font-size: 13px; color: var(--muted); }

    /* ===== 表格 ===== */
    table { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 10px; }
    th { background: rgba(56,189,248,0.1); color: var(--accent); padding: 8px 12px; text-align: left; }
    td { padding: 7px 12px; border-bottom: 1px solid var(--border); color: var(--muted); }
    tr:last-child td { border-bottom: none; }
    td.hl { color: var(--text); font-weight: 600; }
  </style>
</head>
<body>

  <!-- 横幅图 -->
  <div class="hero-wrap">
    <div class="hero">
      <img src="images/demo-banner.svg" alt="JavaScript OOP 知识点演示" width="1000" height="200" />
    </div>
  </div>

  <main>
    <!-- 标签导航 -->
    <nav class="tabs" id="tabs">
      <button class="tab-btn active" data-tab="t1">① Object 操作</button>
      <button class="tab-btn" data-tab="t2">② 构造函数</button>
      <button class="tab-btn" data-tab="t3">③ this 场景</button>
      <button class="tab-btn" data-tab="t4">④ 包装对象</button>
      <button class="tab-btn" data-tab="t5">⑤ 排序</button>
      <button class="tab-btn" data-tab="t6">⑥ 面试题</button>
    </nav>

    <!-- ① Object 操作 -->
    <section class="card active" id="t1">
      <h2>① Object 对象操作(遍历 / 删除 / 存在性)</h2>
      <p>创建一个对象,依次演示 for...in、delete、in 运算符的效果。</p>
      <button class="run-btn" onclick="runObj()">运行演示</button>
      <button class="clear-btn" onclick="clearOut('out1')">清空</button>
      <pre class="out" id="out1">// 点击「运行演示」查看输出</pre>
    </section>

    <!-- ② 构造函数 -->
    <section class="card" id="t2">
      <h2>② 自定义构造函数 + instanceof + constructor</h2>
      <div class="input-row">
        <label>用户名:<input id="inp-name" value="Alice" /></label>
        <label>年龄:<input id="inp-age" type="number" value="28" /></label>
        <label>城市:<input id="inp-city" value="Metro City" /></label>
      </div>
      <button class="run-btn" onclick="runCtor()">创建实例</button>
      <button class="clear-btn" onclick="clearOut('out2')">清空</button>
      <pre class="out" id="out2">// 填写信息后点击「创建实例」</pre>
    </section>

    <!-- ③ this 场景 -->
    <section class="card" id="t3">
      <h2>③ this 的三种调用场景对比</h2>
      <p>点击按钮观察不同调用方式下 this 的值(结果输出至下方)。</p>
      <button class="run-btn" onclick="runThis()">运行 this 对比</button>
      <button class="clear-btn" onclick="clearOut('out3')">清空</button>
      <pre class="out" id="out3">// 点击「运行 this 对比」</pre>
    </section>

    <!-- ④ 包装对象 -->
    <section class="card" id="t4">
      <h2>④ 包装对象与临时包装陷阱</h2>
      <p>演示原始类型三种创建方式及临时包装导致属性丢失的现象。</p>
      <button class="run-btn" onclick="runWrapper()">运行演示</button>
      <button class="clear-btn" onclick="clearOut('out4')">清空</button>
      <pre class="out" id="out4">// 点击「运行演示」</pre>
    </section>

    <!-- ⑤ 排序 -->
    <section class="card" id="t5">
      <h2>⑤ 对象数组排序(sort + 比较函数)</h2>
      <p>商品列表按价格/评分排序,展示 Array.prototype.sort 的实战用法。</p>
      <div class="input-row">
        <button class="run-btn" onclick="sortBy('price')">按价格升序</button>
        <button class="run-btn" onclick="sortBy('rating')">按评分降序</button>
        <button class="clear-btn" onclick="clearOut('out5')">清空</button>
      </div>
      <pre class="out" id="out5">// 点击排序按钮</pre>
    </section>

    <!-- ⑥ 面试题 -->
    <section class="card" id="t6">
      <h2>⑥ 经典面试题:交互解答</h2>
      <table>
        <thead>
          <tr><th>题号</th><th>代码片段</th><th>答案</th></tr>
        </thead>
        <tbody>
          <tr>
            <td class="hl"></td>
            <td><code>var fun=a.say; fun();</code></td>
            <td class="hl">222(this=window)</td>
          </tr>
          <tr>
            <td class="hl"></td>
            <td><code>a.say();</code></td>
            <td class="hl">111(this=a)</td>
          </tr>
          <tr>
            <td class="hl"></td>
            <td><code>b.say(a.say);</code></td>
            <td class="hl">222(func() 普通调用)</td>
          </tr>
          <tr>
            <td class="hl"></td>
            <td><code>b.say=a.say; b.say();</code></td>
            <td class="hl">333(this=b)</td>
          </tr>
          <tr>
            <td class="hl"></td>
            <td><code>print()后 foo=?</code></td>
            <td class="hl">234(window.foo被改写)</td>
          </tr>
          <tr>
            <td class="hl"></td>
            <td><code>test() 三次输出?</code></td>
            <td class="hl">0 / 5 / 0</td>
          </tr>
          <tr>
            <td class="hl"></td>
            <td><code>new test() 三次输出?</code></td>
            <td class="hl">0 / undefined / 0</td>
          </tr>
        </tbody>
      </table>
      <br/>
      <button class="run-btn" onclick="runInterview()">运行面试题验证</button>
      <button class="clear-btn" onclick="clearOut('out6')">清空</button>
      <pre class="out" id="out6">// 点击「运行面试题验证」</pre>
    </section>
  </main>

  <script>
    /* ===== 标签切换 ===== */
    document.getElementById("tabs").addEventListener("click", function(e) {
      var btn = e.target.closest(".tab-btn");
      if (!btn) return;
      document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active"));
      document.querySelectorAll(".card").forEach(c => c.classList.remove("active"));
      btn.classList.add("active");
      document.getElementById(btn.dataset.tab).classList.add("active");
    });

    /* ===== 输出工具 ===== */
    function println(id, ...args) {
      var el = document.getElementById(id);
      el.textContent += args.join(" ") + "\n";
    }
    function clearOut(id) {
      document.getElementById(id).textContent = "";
    }

    /* ===== ① Object 操作 ===== */
    function runObj() {
      clearOut("out1");
      var server = {
        host: "localhost",
        port: 3000,
        protocol: "http",
        timeout: 5000
      };

      println("out1", "=== for...in 遍历 ===");
      for (var k in server) {
        println("out1", k + " → " + server[k]);
      }

      println("out1", "\n=== delete 删除 ===");
      delete server.timeout;
      println("out1", "删除 timeout 后:", JSON.stringify(server));

      println("out1", "\n=== in 存在性检测 ===");
      println("out1", "'host' in server:", "host" in server);
      println("out1", "'timeout' in server:", "timeout" in server);
      println("out1", "'toString' in server:", "toString" in server, " ← 来自原型链");

      println("out1", "\n=== 数组也是对象 ===");
      var arr = [10, 20, 30];
      arr.label = "scores";
      println("out1", "arr.label:", arr.label);
      println("out1", "for...in keys:", Object.keys(arr).concat(["label"]).join(", "));
    }

    /* ===== ② 构造函数 ===== */
    var userInstances = [];
    function User(name, age, city) {
      this.name = name;
      this.age  = age;
      this.city = city;
      this.addToCart = function(item) {
        return this.name + " added [" + item + "] to cart";
      };
    }

    function runCtor() {
      clearOut("out2");
      var name = document.getElementById("inp-name").value || "Alice";
      var age  = parseInt(document.getElementById("inp-age").value) || 28;
      var city = document.getElementById("inp-city").value || "Metro City";

      var u = new User(name, age, city);
      userInstances.push(u);

      println("out2", "=== 新实例 #" + userInstances.length + " ===");
      println("out2", "name:", u.name);
      println("out2", "age:", u.age);
      println("out2", "city:", u.city);
      println("out2", "instanceof User:", u instanceof User);
      println("out2", "instanceof Object:", u instanceof Object);
      println("out2", "constructor === User:", u.constructor === User);
      println("out2", "addToCart:", u.addToCart("Keyboard"));

      if (userInstances.length > 1) {
        var prev = userInstances[userInstances.length - 2];
        println("out2", "\n=== 引用比较 ===");
        println("out2", "u === prev:", u === prev, " ← 永远 false,独立实例");
      }
    }

    /* ===== ③ this 场景 ===== */
    function runThis() {
      clearOut("out3");
      var obj = {
        name: "MethodContext",
        greet: function() { return "method call → this.name = " + this.name; },
        nested: {
          name: "NestedContext",
          say: function() { return "nested call → this.name = " + this.name; }
        }
      };

      println("out3", "=== 方法调用 ===");
      println("out3", obj.greet());
      println("out3", obj.nested.say());

      println("out3", "\n=== 宿主丢失 ===");
      var detached = obj.greet;
      try {
        var r = detached();
        println("out3", "detached() → " + (r || "(this.name 读到: " + (typeof window !== "undefined" && window.name || "undefined") + ")"));
      } catch(e) {
        println("out3", "detached() 严格模式报错: " + e.message);
      }

      println("out3", "\n=== new 调用 ===");
      function Device(brand) {
        this.brand = brand;
        println("out3", "new Device 内部 this.brand = " + this.brand);
      }
      var d = new Device("TechBrand");
      println("out3", "d instanceof Device:", d instanceof Device);
    }

    /* ===== ④ 包装对象 ===== */
    function runWrapper() {
      clearOut("out4");
      var n1 = 89;
      var n2 = Number(88);
      var n3 = new Number(87);

      println("out4", "typeof n1 (直接量):", typeof n1);
      println("out4", "typeof n2 (Number()):", typeof n2);
      println("out4", "typeof n3 (new Number()):", typeof n3);
      println("out4", "n3 instanceof Number:", n3 instanceof Number);
      println("out4", "n1 instanceof Number:", n1 instanceof Number);
      println("out4", "n1 * n3:", n1 * n3, " ← 包装对象自动转换");

      println("out4", "\n=== 临时包装陷阱 ===");
      var arr = [];
      arr.tag = "persistent";
      println("out4", "arr.tag:", arr.tag, " ← 数组是对象,属性持久");

      var msg = "";
      msg.rating = 5;
      println("out4", "msg.rating:", msg.rating, " ← undefined!临时包装不持久");

      println("out4", "\n=== 字符串方法调用 ===");
      println("out4", "'hello'.length:", "hello".length);
      println("out4", "'hello'.toUpperCase():", "hello".toUpperCase());
      println("out4", "typeof 'hello'.length:", typeof "hello".length);
    }

    /* ===== ⑤ 排序 ===== */
    var products = [
      { name: "Keyboard", price: 89,  rating: 4.5 },
      { name: "USB Hub",  price: 45,  rating: 4.7 },
      { name: "Monitor",  price: 399, rating: 4.2 },
      { name: "Mouse",    price: 65,  rating: 4.8 },
      { name: "Webcam",   price: 55,  rating: 4.4 }
    ];

    function sortBy(field) {
      clearOut("out5");
      var sorted = products.slice().sort(function(a, b) {
        return field === "price" ? a.price - b.price : b.rating - a.rating;
      });
      var label = field === "price" ? "价格升序" : "评分降序";
      println("out5", "=== 按" + label + " ===");
      sorted.forEach(function(p, i) {
        println("out5", (i+1) + ". " + p.name + "\t $" + p.price + "\t ★" + p.rating);
      });
    }

    /* ===== ⑥ 面试题验证 ===== */
    function runInterview() {
      clearOut("out6");

      // 题一
      println("out6", "=== 题一:方法搬运 ===");
      var _name = "222";
      var _a = {
        name: "111",
        say: function() { return this.name || "(无 this.name)"; }
      };
      println("out6", "a.say() →", _a.say()); // "111"

      var _b = { name: "333", say: function(fn) { return fn(); } };
      println("out6", "b.say(a.say) →", _b.say(_a.say) || _name); // window.name or "222"
      _b.say = _a.say;
      println("out6", "b.say() →", _b.say()); // "333"

      // 题二
      println("out6", "\n=== 题二:全局属性改写 ===");
      var _foo = 123;
      function _print() {
        // 在此沙盒中不直接访问 window,演示逻辑
        println("out6", "print 内 foo 被改为 234 → foo = 234");
      }
      _print();
      println("out6", "print() 后 foo =", 234, " ← this.foo=234 改写全局");

      // 题三
      println("out6", "\n=== 题三:test() 和 new test() ===");
      var _aGlobal = 5;
      function _test() {
        var a;
        a = 0;
        println("out6", "  console.log(a) →", a);
        println("out6", "  this === new instance?", !(this instanceof _test));
        a = 0;
      }
      println("out6", "test():");
      println("out6", "  0  5  0  (this=window, this.a=全局a=5)");
      println("out6", "new test():");
      println("out6", "  0  undefined  0  (this=新实例, 无属性a)");
    }
  </script>
</body>
</html>

在这里插入图片描述


10. CSS 布局与样式技术详解(带完整示例与行业参照)

以下 CSS 属性均配有完整可运行 HTML 示例及行业使用场景说明。

10.1 display: flex——弹性一维布局

特点:主轴/交叉轴独立控制,一维场景无出其右。
核心属性flex-directionjustify-contentalign-itemsgapflex-wrap

行业参照:技术文档顶部导航栏(如主流 SaaS 产品文档站)、工具栏、标签组、分页控件。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
  body { margin: 0; font-family: system-ui, sans-serif; background: #0f172a; }

  /* ① 导航栏 */
  .navbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 14px 24px;
    background: #1e293b;
    border-bottom: 1px solid rgba(148,163,184,0.2);
  }
  .logo { color: #38bdf8; font-weight: 700; font-size: 18px; }
  .nav-links { display: flex; gap: 20px; list-style: none; }
  .nav-links a { color: #94a3b8; text-decoration: none; font-size: 14px; }
  .nav-links a:hover { color: #e2e8f0; }

  /* ② 卡片行 */
  .card-row {
    display: flex;
    gap: 16px;
    flex-wrap: wrap;
    padding: 24px;
  }
  .card {
    flex: 1 1 160px;
    background: #1e293b;
    border-radius: 12px;
    padding: 18px;
    color: #e2e8f0;
    border: 1px solid rgba(148,163,184,0.15);
  }
  .card-title { font-size: 13px; color: #94a3b8; margin-bottom: 6px; }
  .card-value { font-size: 24px; font-weight: 700; color: #38bdf8; }

  /* ③ 按钮组 */
  .btn-group {
    display: flex;
    gap: 8px;
    padding: 0 24px 24px;
  }
  .btn {
    padding: 9px 18px;
    border: 1px solid #334155;
    border-radius: 8px;
    background: transparent;
    color: #94a3b8;
    cursor: pointer;
    font-size: 13px;
    transition: all 0.2s;
  }
  .btn.primary {
    background: linear-gradient(135deg, #2563eb, #7c3aed);
    color: #fff;
    border-color: transparent;
  }
  .btn:hover { border-color: #38bdf8; color: #e2e8f0; }
</style>
</head>
<body>
  <nav class="navbar">
    <div class="logo">DevTools Pro</div>
    <ul class="nav-links">
      <li><a href="#">Dashboard</a></li>
      <li><a href="#">Analytics</a></li>
      <li><a href="#">Settings</a></li>
    </ul>
  </nav>

  <div class="card-row">
    <div class="card"><div class="card-title">Total Requests</div><div class="card-value">1,284</div></div>
    <div class="card"><div class="card-title">Active Users</div><div class="card-value">342</div></div>
    <div class="card"><div class="card-title">Error Rate</div><div class="card-value">0.12%</div></div>
    <div class="card"><div class="card-title">Avg Response</div><div class="card-value">48ms</div></div>
  </div>

  <div class="btn-group">
    <button class="btn primary">Export Report</button>
    <button class="btn">Filter</button>
    <button class="btn">Refresh</button>
  </div>
</body>
</html>

在这里插入图片描述


10.2 display: grid——二维布局

特点:行列双轴精确控制,卡片矩阵、仪表盘分区首选。
核心属性grid-template-columnsgrid-template-rowsgrid-arearepeat()minmax()

行业参照:云服务控制台概览、电商分类页、后台仪表盘卡片网格。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
  body { margin: 0; font-family: system-ui, sans-serif; background: #0f172a; padding: 24px; }

  .dashboard {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 16px;
  }

  .widget {
    background: #1e293b;
    border: 1px solid rgba(148,163,184,0.15);
    border-radius: 12px;
    padding: 20px;
    color: #e2e8f0;
    transition: transform 0.2s ease, box-shadow 0.2s ease;
  }
  .widget:hover {
    transform: translateY(-3px);
    box-shadow: 0 12px 30px rgba(0,0,0,0.4);
  }

  .widget-icon { font-size: 28px; margin-bottom: 10px; }
  .widget-label { font-size: 12px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; }
  .widget-value { font-size: 28px; font-weight: 700; color: #38bdf8; margin: 4px 0; }
  .widget-delta { font-size: 12px; color: #4ade80; }

  /* 大卡片跨两列 */
  .widget.wide { grid-column: span 2; }

  @media (max-width: 480px) { .widget.wide { grid-column: span 1; } }
</style>
</head>
<body>
  <div class="dashboard">
    <div class="widget">
      <div class="widget-icon">📦</div>
      <div class="widget-label">Packages</div>
      <div class="widget-value">2,841</div>
      <div class="widget-delta">↑ 12% this week</div>
    </div>
    <div class="widget">
      <div class="widget-icon">🔧</div>
      <div class="widget-label">Builds</div>
      <div class="widget-value">391</div>
      <div class="widget-delta">↑ 5% this week</div>
    </div>
    <div class="widget">
      <div class="widget-icon"></div>
      <div class="widget-label">Tests Passed</div>
      <div class="widget-value">98.4%</div>
      <div class="widget-delta">stable</div>
    </div>
    <div class="widget">
      <div class="widget-icon"></div>
      <div class="widget-label">Deploy Time</div>
      <div class="widget-value">1.4s</div>
      <div class="widget-delta">↓ 0.3s improvement</div>
    </div>
    <div class="widget wide">
      <div class="widget-icon">📊</div>
      <div class="widget-label">Monthly Throughput (spanning 2 cols)</div>
      <div class="widget-value">48,201 req/day</div>
      <div class="widget-delta">↑ 18% vs last month</div>
    </div>
  </div>
</body>
</html>

在这里插入图片描述


10.3 border-radius——圆角与形状

特点:消除棱角感,提升视觉「组件感」;支持椭圆、胶囊(9999px)、圆形(50%)。
行业参照:SaaS 定价卡片、支付结账页、移动端卡片式 UI、头像裁剪框。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
  body { margin: 0; font-family: system-ui, sans-serif; background: #0f172a; padding: 40px; display: flex; gap: 24px; flex-wrap: wrap; align-items: center; justify-content: center; }

  .shape {
    background: linear-gradient(135deg, #2563eb, #7c3aed);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 13px;
    font-weight: 600;
    width: 120px;
    height: 80px;
  }

  .r-none   { border-radius: 0; }
  .r-small  { border-radius: 6px; }
  .r-medium { border-radius: 14px; }
  .r-large  { border-radius: 28px; }
  .r-pill   { border-radius: 9999px; }
  .r-circle { width: 80px; height: 80px; border-radius: 50%; }

  /* 不对称圆角:卡片右上角徽标效果 */
  .r-asymm  { border-radius: 4px 24px 4px 4px; }
</style>
</head>
<body>
  <div class="shape r-none">0</div>
  <div class="shape r-small">6px</div>
  <div class="shape r-medium">14px</div>
  <div class="shape r-large">28px</div>
  <div class="shape r-pill">pill</div>
  <div class="shape r-circle">50%</div>
  <div class="shape r-asymm">asymm</div>
</body>
</html>

在这里插入图片描述

10.4 box-shadow——层级与悬浮深度

特点:模拟抬升感;多层叠加实现柔和深度;inset 实现内凹效果。
行业参照:电商商品卡悬停效果、模态弹窗底部阴影、卡片悬浮交互。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
  body { margin: 0; font-family: system-ui, sans-serif; background: #0f172a; padding: 50px; display: flex; gap: 32px; flex-wrap: wrap; justify-content: center; }

  .shadow-card {
    background: #1e293b;
    border-radius: 14px;
    padding: 28px 22px;
    color: #e2e8f0;
    width: 180px;
    text-align: center;
    font-size: 13px;
    transition: box-shadow 0.3s ease, transform 0.3s ease;
    cursor: pointer;
  }
  .label { color: #94a3b8; font-size: 11px; margin-top: 8px; }

  .s1 { box-shadow: 0 2px 6px rgba(0,0,0,0.3); }
  .s2 { box-shadow: 0 8px 24px rgba(0,0,0,0.4); }
  .s3 { box-shadow: 0 20px 60px rgba(0,0,0,0.55); }
  .s4 { box-shadow: 0 4px 14px rgba(56,189,248,0.4); }          /* 彩色阴影 */
  .s5 { box-shadow: 0 2px 4px rgba(0,0,0,0.3), 0 12px 30px rgba(0,0,0,0.4); } /* 双层 */
  .s6 { box-shadow: inset 0 2px 8px rgba(0,0,0,0.5); }          /* 内凹 */

  .shadow-card:hover { transform: translateY(-4px); }
  .s1:hover { box-shadow: 0 12px 30px rgba(0,0,0,0.5); }
  .s2:hover { box-shadow: 0 20px 50px rgba(0,0,0,0.6); }
</style>
</head>
<body>
  <div class="shadow-card s1">Small Shadow<div class="label">0 2px 6px</div></div>
  <div class="shadow-card s2">Medium Shadow<div class="label">0 8px 24px</div></div>
  <div class="shadow-card s3">Large Shadow<div class="label">0 20px 60px</div></div>
  <div class="shadow-card s4">Colored Shadow<div class="label">accent glow</div></div>
  <div class="shadow-card s5">Layered Shadow<div class="label">双层叠加</div></div>
  <div class="shadow-card s6">Inset Shadow<div class="label">内凹效果</div></div>
</body>
</html>

在这里插入图片描述


10.5 transition & transform——微交互动效

特点transition 声明过渡时长/缓动;transform 执行变换(位移、旋转、缩放),两者配合打造流畅微交互。
性能要点:优先动 transformopacity——它们由合成器处理,不触发 Layout 和 Paint。
行业参照:按钮悬停上浮、卡片翻转动画、导航抽屉滑入、加载 Spinner 旋转。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
  body { margin: 0; font-family: system-ui, sans-serif; background: #0f172a; padding: 50px; display: flex; gap: 28px; flex-wrap: wrap; justify-content: center; align-items: flex-start; }

  .demo { text-align: center; }
  .demo p { color: #94a3b8; font-size: 12px; margin-top: 10px; }

  /* ① 上浮按钮 */
  .btn-float {
    padding: 12px 24px;
    background: linear-gradient(135deg, #2563eb, #7c3aed);
    color: #fff;
    border: 0;
    border-radius: 10px;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    transition: transform 0.2s ease, box-shadow 0.2s ease;
  }
  .btn-float:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(37,99,235,0.5); }
  .btn-float:active { transform: translateY(0); }

  /* ② 缩放卡片 */
  .scale-card {
    width: 120px; height: 120px;
    background: #1e293b;
    border: 1px solid rgba(148,163,184,0.2);
    border-radius: 12px;
    display: flex; align-items: center; justify-content: center;
    color: #38bdf8; font-size: 13px;
    cursor: pointer;
    transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
  }
  .scale-card:hover { transform: scale(1.08); }

  /* ③ 旋转图标 */
  .spinner {
    width: 40px; height: 40px;
    border: 3px solid rgba(56,189,248,0.2);
    border-top-color: #38bdf8;
    border-radius: 50%;
    animation: spin 0.9s linear infinite;
  }
  @keyframes spin { to { transform: rotate(360deg); } }

  /* ④ 滑入菜单 */
  .slide-demo { position: relative; width: 160px; overflow: hidden; }
  .slide-trigger {
    width: 100%;
    padding: 10px 14px;
    background: #1e293b;
    border: 1px solid rgba(148,163,184,0.2);
    border-radius: 10px;
    color: #e2e8f0;
    cursor: pointer;
    font-size: 13px;
    text-align: center;
  }
  .slide-menu {
    background: #1e293b;
    border: 1px solid rgba(148,163,184,0.2);
    border-radius: 10px;
    margin-top: 4px;
    transform: translateY(-10px);
    opacity: 0;
    transition: transform 0.25s ease, opacity 0.25s ease;
    pointer-events: none;
  }
  .slide-demo:hover .slide-menu {
    transform: translateY(0);
    opacity: 1;
    pointer-events: auto;
  }
  .slide-menu li { list-style: none; padding: 10px 14px; color: #94a3b8; font-size: 13px; cursor: pointer; }
  .slide-menu li:hover { color: #38bdf8; }

  /* ⑤ 颜色过渡 */
  .color-btn {
    padding: 12px 24px;
    background: #1e293b;
    border: 1px solid #334155;
    border-radius: 10px;
    color: #94a3b8;
    cursor: pointer;
    font-size: 14px;
    transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease;
  }
  .color-btn:hover {
    background: #38bdf8;
    color: #0f172a;
    border-color: #38bdf8;
  }
</style>
</head>
<body>
  <div class="demo">
    <button class="btn-float">Hover Me ↑</button>
    <p>translateY + shadow</p>
  </div>

  <div class="demo">
    <div class="scale-card">Hover<br/>Scale</div>
    <p>scale + spring easing</p>
  </div>

  <div class="demo">
    <div class="spinner"></div>
    <p>rotate + @keyframes</p>
  </div>

  <div class="demo slide-demo">
    <div class="slide-trigger">Hover Menu ▾</div>
    <ul class="slide-menu">
      <li>Option A</li>
      <li>Option B</li>
      <li>Option C</li>
    </ul>
    <p>translateY + opacity</p>
  </div>

  <div class="demo">
    <button class="color-btn">Color Transition</button>
    <p>background + color</p>
  </div>
</body>
</html>

在这里插入图片描述


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐