Skip to content

Object 对象与构造函数(Day8 3/4) ​

本页对应 Day08_JavaScript,共 6 段视频,约 59 分钟。

学习目标 ​

  • 能够说明构造函数中 return 原始类型数据与对象类型数据时,new 实例化结果的差异,并编写代码验证。
  • 能够区分同一函数的“调用”和“实例化”用法,并解释其结果不同的原因及如何使二者结果一致。
  • 能够说明 Number、String、Boolean 原始类型数据的值状态与对象状态,并验证其可自动转换的特性。
  • 能够解释原始类型数据被当作对象使用时临时包装、属性添加后随即销毁的机制,并预测相关代码输出。
  • 能够指出 this 在全局环境、普通函数调用和对象方法调用中的不同取值,并说明其只读特性。
  • 能够说明 window 作为浏览器环境全局对象的含义,并验证全局变量是 window 属性的关系。

知识点 ​

广义对象与狭义对象:先把“对象”说清楚 ​

为什么需要: 学 Object 最先容易混的是“对象”这个词。日常说“一切皆对象”,但笔记又强调 Object 数据类型只是对象类型中的一种。如果不先区分广义和狭义,后面看到数组、函数、Object 都会打架。

怎么想: 广义理解是“一切皆对象”,数组、函数都是对象的一种;狭义理解是“Object 数据类型”,它是对象类型中的一种,与 Array、Function 是平级的。也就是说,广义的“对象”是上位概念,狭义的 Object 是具体类型之一。

怎么做: 本节笔记给的两个判断要保留:

js
一切皆对象,数组、函数都是对象的一种。
js
Object 数据类型,是对象类型中的一种,与Array、Function 是平级的。

易错点: 不要把“数组是对象”理解成“数组就是 Object 类型”。数组是 Array 类型,函数是 Function 类型;它们都属于广义对象,但不等于狭义的 Object 数据类型。

构造函数的返回值:new 出来的到底是谁 ​

为什么需要: 既然对象常由构造函数创建,就要知道构造函数里写 return 会不会改变 new 的结果。转写稿里专门用 Product 做实验:不写 return、return 100、return 数组,结果不同。

怎么想: 规则是人为设计的语法规则:如果构造函数中没有 return,或者 return 的是原始类型数据(number、string、boolean 等),实例化结果就是根据构造函数创建的新对象;如果 return 的是对象类型数据(广义对象,如数组、函数、Object),实例化结果就是 return 的那个对象,不再创建新对象。

怎么做: 可以先写一个空构造函数,new Product() 得到新对象;写 return 100,结果仍是新对象;写 return [],结果就变成那个数组。转写稿还提醒:构造函数里的语句照样执行,return 也能中断构造函数,只是返回值规则要按上面判断。

易错点: 不要把“调用”和“实例化”混了:不带 new 是调用,带 new 是实例化。return 原始类型不会改变实例化结果,但 return 对象类型会直接替换结果。

函数与构造函数:一个东西,看你怎么用 ​

为什么需要: 看到 Array() 和 new Array() 结果相似,容易以为函数和构造函数是两个东西。转写稿用 user、Product 对比说明:它们本质是同一个函数,只是用法不同。

怎么想: 函数名后加小括号调用,它就是函数;用 new 去实例化,它就是构造函数。所有函数都是大写的 Function 的实例;而 Function 的构造函数又是它自己,这是 JS 设计里逻辑自洽但看起来奇怪的一环。

怎么做: 写 function user(){ console.log('hello') },new user() 会执行函数体并得到新对象;user() 也会执行函数体,但没有 return 时得到 undefined。若想让调用和实例化结果一样,可以在构造函数里 return {} 或 return function(){} 这类对象类型数据。

易错点: 不要因为“构造函数”这个名字就以为它不能当普通函数调用;也不要以为调用函数会自动产生新对象。结果一样与否,取决于 return 的数据类型。

原始类型也有对象状态:Number、String、Boolean ​

为什么需要: 前面说“一切皆对象”,但 number、string、boolean 又被划为原始类型。转写稿用 new Number()、Number()、直接量说明:原始类型有值状态,也能出现对象状态。

怎么想: 直接量和 Number() 创建的是值的状态;new Number() 创建的是对象状态。原始类型在需要当对象用时,会自动包装成对象;需要当值用时,又会转回值。null 和 undefined 没有对象状态,也没有对应的构造函数可用。

怎么做: 转写稿示例:let n01 = 89、let n02 = Number(88)、let n03 = new Number(87)。n01 * n03 会把 n03 当值参与运算;n01.constructor 会把 n01 临时包装成对象再取属性。字符串直接量也能点 .constructor、.length。

易错点: 整数直接量后直接写点可能造成解析歧义,如 12.constructor 可能被当成小数点;可用 (12).constructor 或 12..constructor。null、undefined 不能这样用。

临时包装对象:字符串加属性为什么读不到 ​

为什么需要: 这是转写稿里的“世纪巨坑”:给字符串加属性,语法不报错,但读出来是 undefined。理解它才能真正理解原始类型的对象特性。

怎么想: 原始类型默认是值状态。用点操作时,JS 会临时用 String、Number、Boolean 创建一个包装对象,属性加到或读到这个临时对象上;语句执行完临时对象就销毁。下一次再用点,又会创建新的临时对象,所以上一次加的属性找不到。

怎么做: 转写稿对比:数组 arr.address = '上海' 能读到,因为数组本身就是对象类型;字符串 msg.address = '上海' 后再读 msg.address 得到 undefined。原因就是两次点操作对应两个不同的临时包装对象。

易错点: 不要以为“没报错就等于加上了”。原始类型上加属性只是加在临时对象上,不会留在原始值本身。

this 与 window:对象体系的入口 ​

为什么需要: 转写稿用 this 和 window 说明对象体系:全局 this 是 window,对象方法里的 this 是该对象;浏览器里 window 是全局对象,所有全局变量都是 window 的属性。

怎么想: this 是系统内置的只读变量,不同地方值不同;不能给 this 赋值,也不能拿 this 当变量名。window 在浏览器打开时自动创建,是全局对象;全局变量可直接用,也可写成 window.address,通常省略 window.。系统函数如 parseInt、alert、Number、String、Boolean、Object、Array、Function 也都是 window 的属性。

怎么做: 在全局 console.log(this) 得到 window;在对象方法里 console.log(this) 得到该对象。声明全局变量 address 后,可用 window.address 验证。运行在 Node.js 时全局对象不是 window,因为全局对象由宿主提供。

易错点: 不要把 this 当成普通变量去赋值;也不要把 window 当成所有环境都有的全局对象,它取决于 JS 运行在浏览器还是 Node.js。

构造函数返回值:先看规则,再看现象 ​

为什么需要:实例化 new Product() 到底得到什么,不能靠猜。需要明确 return 对实例化结果的影响,否则会把“构造函数”和“普通函数”混在一起。

怎么想:先区分“调用”和“实例化”。不带 new 是调用;带 new 是实例化,实例化表达式会产生一个结果。构造函数的 return 是否影响这个结果,要看返回的数据类型。

怎么做:先写一个没有 return 的构造函数:

js
function Product() {}
var p = new Product();
console.log(p); // Product 的实例,新对象

再写返回原始类型:

js
function Product() {
  return 100;
}
var p = new Product();
console.log(p); // 仍是 Product 实例

最后写返回对象类型:

js
function Product() {
  return [1, 2, 3];
}
var p = new Product();
console.log(p); // [1, 2, 3]

规则是:构造函数中没有 return,或 return 的是原始类型数据(number、string、boolean 等),实例化结果就是根据构造函数创建的新对象;如果 return 的是对象类型数据(数组、函数、普通对象等广义对象),实例化结果就是 return 的那个对象。return 仍然能中断构造函数内部语句的执行,这一点和普通函数一样。

易错点:return 100 不会让 new Product() 得到 100;只有返回对象类型才会替换实例化结果。不要把“中断执行”和“改变实例化结果”混为一谈。

构造函数和函数:一个东西,两种用法 ​

为什么需要:Array、Object 等既能调用又能 new,容易让人以为它们是两套东西。其实函数和构造函数是一体两面。

怎么想:看名字后面怎么用。加 new 就是实例化,当构造函数用;直接加小括号就是调用,当普通函数用。

怎么做:

js
function User() {
  console.log('hello user');
}
var r1 = new User();
var r2 = User();
console.log(r1); // User 实例,新对象
console.log(r2); // undefined

想让调用和实例化结果一样,可以让构造函数返回对象类型:

js
function Product() {
  return {};
}
var p1 = Product();
var p2 = new Product();
console.log(p1, p2); // 都是空对象

易错点:返回原始类型不能让两者一致;只有返回对象类型,new 的结果才会被替换成该对象。

原始类型的对象特性:值状态与对象状态 ​

为什么需要:JS 说“一切皆对象”,但 number、string、boolean 又被划为原始类型。需要理解它们如何同时满足这两套说法。

怎么想:原始类型有“值状态”,需要当对象用时又能出现“对象状态”。number、string、boolean 都有对象状态;null、undefined 没有。

怎么做:

js
var n01 = 89;
var n02 = Number(88);
var n03 = new Number(87);

console.log(typeof n01); // number
console.log(typeof n02); // number
console.log(typeof n03); // object
console.log(n01 instanceof Number); // false
console.log(n03 instanceof Number); // true
console.log(n01 * n03); // 7743,n03 自动转为值状态
console.log(n01.constructor); // Number,n01 自动包装成对象

易错点:整数直接量写 12.constructor 会语法错误,因为点和小数点有歧义;可写 (12).constructor、12..constructor,或先用变量保存。null、undefined 不能这样用。

临时包装对象:字符串加属性为什么读不到 ​

为什么需要:给字符串加属性再读取,结果常是 undefined,这是经典坑。

怎么想:原始类型用点操作时,会临时创建一个包装对象;属性加到临时对象上,语句结束临时对象销毁。下一次读取又重新创建临时对象,所以读不到上一次加的属性。

怎么做:

js
var arr = [];
arr.address = '上海';
console.log(arr.address); // 上海

var msg = 'hello';
msg.address = '上海';
console.log(msg.address); // undefined

易错点:数组本身是对象类型,属性可保留;字符串、数字、布尔是原始类型,临时包装对象用完即销毁。

this 与 window:全局对象和只读变量 ​

为什么需要:this 的值在不同位置不同,window 又是浏览器环境的全局对象,必须先认识它们。

怎么想:在全局直接输出 this,浏览器中是 window;普通函数调用时也是 window;对象方法中的 this 是该对象。this 不能赋值,也不能当变量名。

怎么做:

js
console.log(this); // window

function fn() {
  console.log(this);
}
fn(); // window

var obj = {
  getThis: function () {
    console.log(this);
  }
};
obj.getThis(); // obj

window 表示浏览器窗口,打开浏览器时自动创建,是浏览器环境下的全局对象。所有全局变量都是 window 的属性,所以 window.address 和 address 等价,通常省略 window.。系统函数如 parseInt、alert、Number、Array、Object、Function 也都是全局变量,即 window 的属性。

易错点:this 的值不是固定不变;window 是浏览器宿主提供的全局对象,换到 Node.js 环境就不是 window。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:构造函数的返回值规则 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>构造函数的返回值</title>
</head>
<body>
  <script>
    // 1. 没有 return:实例化结果是由构造函数创建的新对象
    function Product() {}
    var p1 = new Product();
    console.log('无 return:', p1); // Product {}

    // 2. return 原始类型数据(number/string/boolean/null/undefined):不影响实例化结果
    function Product2() {
      return 100;
    }
    var p2 = new Product2();
    console.log('return 100:', p2); // Product2 {}

    // 3. return 对象类型数据(广义对象:数组、函数、{}):实例化结果就是该对象
    function Product3() {
      return [];
    }
    var p3 = new Product3();
    console.log('return []:', p3); // []

    function Product4() {
      return {};
    }
    var p4 = new Product4();
    console.log('return {}:', p4); // {}

    // 4. return 依然可以中断构造函数体的执行
    function Product5() {
      console.log('这句会执行');
      return 100;
      console.log('这句不会执行');
    }
    new Product5();
  </script>
</body>
</html>

要点解释与易错点:

  • new Product() 才叫“实例化”,Product() 叫“调用”;实例化结果是一个由构造函数创建的新对象。
  • 构造函数中没有 return 或 return 的是原始类型数据(number、string、boolean 等)时,new 的结果仍是新创建的对象,return 的值被忽略。
  • 构造函数中 return 的是对象类型数据(数组、函数、{} 都属于广义对象)时,new 的结果就是那个 return 出来的对象,此时不会创建新实例。
  • 易错点:return 100 看起来“有返回值”,但它不会替换实例化结果;只有对象类型才会替换。这是 JS 语法规则的人为设定,不需要纠结合理性,记住规则即可。
  • 易错点:return 虽然不影响实例化结果(原始类型时),但它中断构造函数执行的功能没有变化,return 后面的代码不会执行。

示例二:构造函数和函数是一体两面 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>构造函数与函数</title>
</head>
<body>
  <script>
    // 同一个函数,调用它是函数,new 它它就是构造函数
    function User() {
      console.log('hello user');
    }

    var res1 = new User(); // 实例化:函数体执行,得到新对象
    var res2 = User();     // 调用:函数体执行,得到 return 的值(没有 return 即 undefined)

    console.log('res1 =', res1); // User {}
    console.log('res2 =', res2); // undefined

    // 如何让“调用”和“实例化”结果一样?在函数里 return 一个对象类型数据
    function Product() {
      return {}; // 对象类型数据
    }

    var p1 = Product();      // 调用,返回 {}
    var p2 = new Product();  // 实例化,return 的是对象,结果也是 {}

    console.log('p1 =', p1); // {}
    console.log('p2 =', p2); // {}

    // 系统内置的 Array 就是这样:调用和实例化结果都是一个数组
    console.log(Array(1, 2, 3));     // [1, 2, 3]
    console.log(new Array(1, 2, 3)); // [1, 2, 3]

    // 所有函数都是 Function 的实例;Function 的构造函数是它自己
    console.log(User.constructor === Function);     // true
    console.log(Function.constructor === Function); // true
  </script>
</body>
</html>

要点解释与易错点:

  • User 本身就是一个函数,new User() 时它扮演构造函数角色,User() 时它扮演普通函数角色,东西是同一个东西,看你怎么用。
  • 调用没有 return 的函数得到 undefined;实例化得到的是新对象,两者结果不同。
  • 想让“调用”和“实例化”结果一致,就在函数体内 return 一个对象类型数据(如 {}、[]、函数);return 原始类型无效。
  • 易错点:new User() 时函数体照样会执行,不要以为实例化就不执行函数内部语句。
  • 易错点:所有函数都是 Function 的实例(User.constructor === Function),而 Function.constructor === Function,即 Function 自己构造了自己,这是 JS 中比较“逆天”的设计,逻辑自洽但不必深究。

示例三:原始类型数据的临时包装(世纪巨坑) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>原始类型临时包装成对象</title>
</head>
<body>
  <script>
    // 数组是对象类型,可以正常添加、读取属性
    var arr = [];
    arr.address = '上海';
    console.log('arr.address =', arr.address); // 上海

    // 字符串是原始类型,添加属性后读取不到 —— 世纪巨坑
    var msg = 'hello';
    msg.address = '上海';
    console.log('msg.address =', msg.address); // undefined

    // 原因:每次“点”操作都会临时 new String(msg) 出一个新对象,
    // 用完即销毁;两次操作不是同一个临时对象
    console.log('msg.length =', msg.length); // 5,临时对象上有 length
    console.log('"hello".constructor === String:', 'hello'.constructor === String); // true

    // 原始类型的两种状态:值状态 与 对象状态
    var n01 = 89;              // 直接量:值状态
    var n02 = Number(88);      // Number 函数:值状态
    var n03 = new Number(87);  // new Number:对象状态

    console.log(n01, n02, n03);       // 89 88 [Number: 87]
    console.log(n01 + n03);           // 176,n03 自动转为值状态
    console.log(n03 * n01);           // 7743,n03 自动转为值状态
    console.log(n01.constructor);     // [Function: Number],n01 自动包装成对象
    console.log('constructor' in n03); // true,n03 本身就是对象状态
    console.log('constructor' in n01); // false,in 不触发包装

    // 数字直接量后接点会与小数点产生歧义,这是纯语法错误
    // console.log(12.constructor);   // 报错:SyntaxError
    console.log((12).constructor);    // 正确:加括号
    console.log(12..constructor);     // 正确:两个点,一个小数点一个属性点
    console.log(12.5.constructor);    // 正确:小数没有歧义
  </script>
</body>
</html>

要点解释与易错点:

  • 原始类型(number、string、boolean)既有值状态,也有对象状态;new Number() 得到的是对象状态,直接量和 Number() 得到的是值状态。
  • 当把原始类型当对象用(点属性、点方法)时,JS 会临时包装成对应对象(如 new String(msg)),属性添加/读取都发生在这个临时对象上,语句执行完临时对象即销毁。
  • 因此 msg.address = '上海' 加到了一个转瞬即逝的临时对象上,下一行 msg.address 又临时创建了另一个新对象,自然读不到,得到 undefined。
  • 数组没有这个坑,因为数组本身就是对象类型,没有“值状态”需要转换,属性直接加在数组对象自身上。
  • 易错点:12.constructor 报语法错误,浏览器分不清点是小数点还是属性访问点;解决方式是 (12).constructor 或 12..constructor,变量形式(如 n01.constructor)不会产生歧义。
  • 易错点:in 运算符不会触发临时包装,所以 'constructor' in n01 为 false,而 'constructor' in n03 为 true(n03 本身就是对象)。

常见错误 ​

  1. 现象:new F() 里写了 return 100,结果仍是实例。→ 原因:构造函数返回原始类型会被忽略。→ 修法:要替换实例结果,必须 return 对象类型(对象/数组/函数)。
  2. 现象:F() 得到 undefined,new F() 得到实例,误以为二者应一致。→ 原因:调用取返回值,实例化按 new 规则取结果。→ 修法:先明确是否使用 new;若需一致,在构造函数中返回对象。
  3. 现象:let s='abc'; s.x=1; console.log(s.x) 得到 undefined。→ 原因:赋值加在临时包装对象上,语句结束即销毁,读取时又新建临时对象。→ 修法:把属性放到普通对象/数组等真实对象上,不要依赖原始值持久保存属性。
  4. 现象:12.constructor 报语法错误,而 12.2.constructor 正常。→ 原因:整数后的点被解析为小数点,产生歧义。→ 修法:写成 (12).constructor、12..constructor,或先存入变量。
  5. 现象:对 null/undefined 点属性或取 constructor 报错。→ 原因:只有 number/string/boolean 有临时对象包装,null/undefined 没有对象状态。→ 修法:先判空,再访问属性。
  6. 现象:对象方法里以为 this 一定是 window。→ 原因:this 随调用上下文变化。→ 修法:按调用方式判断;不要给 this 赋值,也不要把它当普通变量改名。

练习 ​

  1. 写构造函数 Product,使 Product() 与 new Product() 返回同类结果,并说明 return 应写什么。
  2. 判断输出并解释:function User(){ return 100 } const a=new User(); const b=User(); console.log(a,b)。
  3. 排查并改造:let s='abc'; s.x=1; console.log(s.x) 为什么是 undefined?给出能保存 x 的方案。
  4. 修正 console.log(12.constructor) 的语法错误,写出两种可行写法。
  5. 判断两次输出并说明原因:const obj={m(){console.log(this===obj)&#125;&#125;; obj.m(); const f=obj.m; f();。

原视频对照 ​

#视频时长
1构造函数的返回值问题07:27
2构造函数和函数14:26
3原始类型数据的对象特性15:37
4原始类型数据临时包装成对象08:29
5认识this05:38
6window07:56

小结 ​

  1. new 表达式的结果优先看构造函数 return:原始值忽略,对象值替换实例。
  2. 函数与构造函数是同一函数值的两种使用方式,区别在调用方式,不在定义。
  3. 原始类型的“对象特性”是临时包装,属性不可持久;null/undefined 不适用。
  4. 整数直接量点属性易触发语法歧义,括号、双点或变量可规避。
  5. this 是只读且随调用上下文变化,不能赋值或作为变量名。
  6. 浏览器中 window 是全局对象,全局变量是其属性,window. 通常可省略;宿主变化时全局对象也变化。
  7. 调试对象/属性问题先区分“值状态/对象状态”和“是否新建了临时对象”。