Skip to content

Object 对象(Day8 2/4) ​

本页对应 Day08_JavaScript,共 6 段视频,约 1 小时 6 分。

学习目标 ​

  • 能说明 Object 类型对象为何被称为“最纯净的对象”,并举例验证数组、函数同样具备对象的基本属性操作。
  • 能用自己的话解释构造函数与数据类型的一一对应关系,并判断给定对象的构造函数。
  • 能区分“构造函数”与“实例/对象”的关系,说明一个构造函数可创建多个独立实例。
  • 能使用 instanceof 和 constructor 判断数组、函数、普通对象、日期对象的构造函数,并解释 instanceof Object 的特殊表现。
  • 能使用 new 实例化内置构造函数,并说明数组字面量、对象字面量与对应实例化写法的关系。
  • 能编写自定义构造函数,通过 this 为实例设置属性和方法,并使用 new 创建多个属性值不同的实例进行验证。

知识点 ​

先说明本节回顾的边界 ​

本节笔记标题是“1 回顾”,列出的是函数知识框架;转写稿大量内容在讲对象、构造函数、实例化。两者并非逐条对应。下面以笔记的9个回顾点为骨架,只把转写稿中能确认的“构造函数也是函数、函数本身也是对象”补进相关易错点,不虚构课堂未讲内容。转写稿里的“便利”应写作“遍历”,“够造函数”应写作“构造函数”,“湿力化/实力化”应写作“实例化”。

函数的组成:先看一张“零件表” ​

为什么需要:写函数前先知道它由什么构成,才不会把“函数名、参数、函数体、返回值”混成一团。怎么想:函数名是引用入口,参数是输入,函数体是过程,返回值是输出。怎么做:声明时写清参数,函数体内用return给出结果;没有return时返回undefined。易错点:return后的代码不执行;函数名不加括号表示函数对象,加括号才是调用。

调用与返回值:函数不是“摆着看”的 ​

为什么需要:函数定义只是准备,调用才产生结果。怎么想:先确认实参,再执行函数体,最后接收返回值。怎么做:用函数名(实参)调用;需要结果就接收返回值。易错点:把“函数对象”和“调用结果”混用,例如把fn当fn()用;无返回值却硬接收具体值。

参数:形参、实参、数量、默认值、arguments ​

为什么需要:参数决定函数能否复用。怎么想:形参是声明时的占位,实参是调用时传入的值;数量不必相等。怎么做:缺省实参时形参为undefined,可用默认值或在函数体内兜底;arguments可读取调用时传入的实参列表。易错点:arguments是类数组,不是真数组;默认值不是“实参必须写满”;形参名与实参变量名不是一回事。

作用域与作用域链:变量从哪里找 ​

为什么需要:不知道作用域,就会误以为函数内部能随便改外部变量。怎么想:局部变量在函数内,全局变量在外部;查找变量由内向外沿作用域链找。怎么做:优先用局部变量减少污染;需要共享时再放外层。易错点:内层找不到才向外找,不是反过来;函数内未声明就赋值可能变成全局变量(非严格模式下尤其要注意)。

提升:var提升与function提升 ​

为什么需要:提升解释了“代码还没写到,为什么变量能访问/函数能调用”。怎么想:var声明会提升,赋值不提升;函数声明会整体提升。怎么做:先声明后使用,避免依赖提升。易错点:var提升后值是undefined;函数表达式不会像函数声明那样整体提升;构造函数首字母大写只是约定,不是语法。

匿名函数与IIFE:函数也可以“不具名、立即跑” ​

为什么需要:匿名函数便于作为值传递;IIFE用于制造独立作用域,避免变量污染。怎么想:匿名函数常出现在赋值、回调中;IIFE是定义后立刻调用。怎么做:var f = function(){};IIFE写成(function(){})()。易错点:匿名函数表达式不能像函数声明那样整体提升;IIFE外层括号不能少,否则容易被当成函数声明。

回调函数与递归:把函数当参数、让自己调自己 ​

为什么需要:回调让函数在合适时机执行;递归解决可分解为同类子问题的问题。怎么想:回调关注“传函数本身”,递归关注“终止条件+递推”。怎么做:回调传函数名或函数表达式;递归先写终止条件,再写向终止条件靠近的调用。易错点:fn和fn()不同;递归没有终止条件会栈溢出。

转写稿补充:构造函数也是函数,函数本身也是对象 ​

为什么需要:很多同学把“构造函数”当成特殊语法,其实转写稿反复强调:构造函数本质就是函数。怎么想:转写稿先给数组/函数加属性,用for in遍历,再用instanceof和constructor判断,推导出“函数也是对象”;因此自定义构造函数只是“用函数描述对象”。怎么做:转写稿中可确认的调试代码整理如下:

js
function fn() {}
fn.age = 18;
fn.address = '上海';
for (var attr in fn) {
  console.log(attr, fn[attr]); // 可遍历到 age、address
}
console.log(fn instanceof Function); // true
console.log(fn.constructor === Function); // true

function User(name, age, address) {
  this.name = name;
  this.age = age;
  this.address = address;
}
var u1 = new User('高小乐', 78, '上海');
var u2 = new User('曹操', 79, '北京');
console.log(u1 instanceof User); // true
console.log(u1.constructor === User); // true
console.log(u1 === u2); // false

易错点:instanceof Object对很多对象成立,不能只靠它区分具体类型;constructor可看来源,但原型链被改写时可能不可靠;new出来的两个实例即使长得一样也不相等,因为各有独立内存空间。把这一条放回“回顾”,就是提醒:函数既能被调用,也能作为对象被添加属性;但复习函数时仍要先抓住“组成、参数、作用域、提升、调用方式”这条主线。

先分清“广义对象”和“狭义对象” ​

本节笔记先给出两个口径,代码原样保留:

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

为什么需要: 初学者最容易把“对象”混着用:一会儿说数组是对象,一会儿又说 Object 是对象。先分清广义、狭义,后面的 for...in、delete、constructor、instanceof 才不会乱。
怎么想: 广义看“它是不是对象这一类”;狭义看“它的构造函数是谁、它是哪种具体数据类型”。
怎么做: 说“数组、函数都是对象”时用广义;说“Object 与 Array、Function 平级”时用狭义。
易错点: 不能说“数组不是对象”,也不能把狭义 Object 和广义“对象”混为一谈。

Object 为什么叫“最纯净的对象” ​

为什么需要: 数组、函数明明也是对象,为什么偏偏 Object 类型的对象常直接叫“对象”?需要理解它只具备对象最基本特征,没有额外特点。
怎么想: 把对象基本特征想成“底座”。数组在底座上加了连续索引和 length;函数在底座上加了“可调用”。Object 没有这些额外特点,所以最纯净。
怎么做: 记住:Object 只保留对象基本特征;数组、函数是在对象基本特征上增加自己的特性。
易错点: 不要因为数组有 length、函数能调用,就否定它们也是对象;它们是“加了特点的对象”。

遍历、删除、判断属性:同一套操作为何能用于数组和函数 ​

为什么需要: 如果数组、函数也是对象,那么给对象用的属性操作,应该也能用于它们。转写稿正是用数组和函数来验证这一点。
怎么想: 先验证“能不能加属性”,再验证“能不能遍历、删除、判断”。数组元素在内部也可被看成属性:索引是属性名,元素值是属性值。
怎么做: 可以给数组加自定义属性,再用 for...in 遍历:

js
const arr = [100, 200, 300, 400];
arr.username = "小乐";
arr.address = "上海";

for (const key in arr) {
  console.log(key, arr[key]);
}

调试时会看到 0 1 2 3 username address,但 length 没被遍历出来,因为 length 不允许被枚举。接着可验证:

js
console.log("username" in arr); // true
delete arr.address;
console.log("address" in arr);  // false

删除数组元素 delete arr[2] 也能删掉值,但索引位置还在,长度不变,不会自动重排。
易错点: for...in 遍历的是属性名,数组索引也会变成字符串键;delete 删数组元素不等于 splice,会留下空位;length 是数组自带特性,不参与 for...in。

构造函数与数据类型:对象从哪里来 ​

为什么需要: 内置类型不够描述“用户”“商品”等复杂数据,所以需要构造函数来描述一类对象有什么属性和方法。
怎么想: 构造函数像模具或配置清单:它描述对象特点,对象是根据它造出来的实例。相同数据类型的对象,构造函数相同;构造函数对应数据类型。
怎么做: 记住常见对应:数组的构造函数是 Array,函数的构造函数是 Function,普通对象的构造函数是 Object,日期对象的构造函数是 Date。
易错点: 构造函数不是“构造一个函数”,而是“用来构造对象的函数”;对象是实例,构造函数是描述。

instanceof 与 constructor:判断对象来自哪个构造函数 ​

为什么需要: 看到一个对象,常要判断它是不是数组、函数、日期等。
怎么想: instanceof 判断“对象是不是某构造函数的实例”;constructor 属性可直接拿到该对象的构造函数。
怎么做:

js
console.log([] instanceof Array);        // true
console.log([] instanceof Object);       // true
console.log({} instanceof Array);        // false
console.log([].constructor === Array);   // true

转写稿特别提醒:数组、函数、日期用 instanceof Object 都为 true,这涉及原型链,后面再解释;但反过来不能乱推。
易错点: instanceof 左边是对象、右边是构造函数;constructor 是系统给对象添加的属性,不是自己设置的。

new 与实例化:每次创建都是独立对象 ​

为什么需要: 构造函数只是描述,不能直接当成品用,要通过 new 创建实例。
怎么想: new 一次构造函数,就产生一个新对象;每个对象有独立内存空间,长得一样也不相等。
怎么做: new Array() 可创建数组,[] 是它的简写;new Object() 可创建普通对象,{} 是简写。自定义构造函数一般没有这种简写。
易错点: new Array() 与 [] 内容相同但不等于同一个对象;两个空数组也不相等,因为它们是两个独立个体。

自定义构造函数:用 this 描述未来实例 ​

为什么需要: 想描述本站用户,可以自定义 User 构造函数,让每个用户实例都有姓名、年龄、地址和方法。
怎么想: 构造函数内部用 this 指向未来 new 出来的实例;形参负责接收每个实例不同的值,不要把值写死。
怎么做:

js
function User(name, age, address) {
  this.name = name;
  this.age = age;
  this.address = address;
  this.addShopCart = function (product) {
    console.log(this.name + "将" + product + "加入购物车");
  };
}

const u1 = new User("小乐", 18, "上海");
u1.addShopCart("奶粉");

易错点: 构造函数本质仍是函数,但当前先按“构造函数”理解;首字母大写只是约定,不是语法;this.name 是实例属性,形参 name 是局部变量,不是一回事;调用时建议加 new。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

代码一:Object 是最纯净的对象(数组、函数也能当对象用) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Object 是最纯净的对象</title>
</head>
<body>
<script>
  /* ---------- 1. 数组:在对象基本特征之上加了自己的特点 ---------- */
  var arr = [100, 200, 300, 400, 500];

  // 给数组加属性(点语法 / 中括号都可以)
  arr.userName = '小乐';
  arr.address  = '上海';
  console.log(arr.userName);    // 小乐
  console.log(arr['address']);  // 上海

  // for in 遍历:元素索引 0~4 也被当成属性名,length 不会被遍历出来
  for (var attr in arr) {
    console.log(attr, arr[attr]); // 0 100 … 4 500  userName 小乐  address 上海
  }

  // delete 删除属性(一元运算符)
  delete arr.address;
  console.log('address' in arr); // false
  console.log('length'  in arr); // true

  // delete 删除元素:索引必须连续,会留一个空位,length 不变
  delete arr[2];
  console.log(arr);        // [100, 200, empty, 400, 500]
  console.log(arr.length); // 5

  /* ---------- 2. 函数也是对象,操作一模一样 ---------- */
  function func() {}

  // 注意:func.name 是函数自带的属性,且不允许被遍历,所以这里换名
  func.userName = '小乐';
  func.age      = 18;
  func.address  = '上海';

  for (var key in func) {
    console.log(key, func[key]); // userName 小乐  age 18  address 上海
  }

  console.log('age'    in func); // true
  console.log('gender' in func); // false

  delete func.address;
  console.log('address' in func); // false
</script>
</body>
</html>

关键行与易错点

  • Object 类型是“最纯洁的对象”,只有对象最基本特征;数组、函数是在这个基础上加了自己的特点(数组有连续索引元素和 length,函数能被调用),所以针对 Object 的遍历、删除、判断,对数组和函数同样成立。
  • for in 遍历的是属性:数组元素被当成属性名 0、1、2… 处理,所以索引也会被遍历出来;而 length 是数组自带的、不允许被遍历的属性,所以不会出现。
  • delete 是一元运算符,删除属性后 in 判断为 false;但 delete arr[2] 只删值不删位,数组会留下空位,length 仍是 5 —— 这是数组“索引必须连续”这个自身特点导致的。
  • 给函数加属性时别用 name:func.name 是函数自带且不可遍历的,直接赋值会覆盖它且遍历不出来,换个名字才能看到效果。

代码二:判断对象的构造函数(instanceof 与 constructor) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>判断对象的构造函数</title>
</head>
<body>
<script>
  // 创建 4 个对象,它们的构造函数各不相同
  var arr  = [1, 2, 3];
  var fn   = function () {};
  var user = { name: '小乐', age: 18, address: '上海' };
  var date = new Date();

  /* ---------- 方式一:instanceof(二元运算符) ---------- */
  console.log(arr  instanceof Array);    // true
  console.log(arr  instanceof Function); // false
  console.log(fn   instanceof Function); // true
  console.log(fn   instanceof Array);    // false
  console.log(date instanceof Date);     // true
  console.log(date instanceof Function); // false

  // Object 的特殊性:所有对象 instanceof Object 都成立
  console.log(arr  instanceof Object);   // true
  console.log(fn   instanceof Object);   // true
  console.log(date instanceof Object);   // true
  // 反过来不成立
  console.log(user instanceof Array);    // false

  /* ---------- 方式二:constructor 属性(系统自带,每个对象都有) ---------- */
  console.log(arr.constructor  === Array);    // true
  console.log(fn.constructor   === Function); // true
  console.log(user.constructor === Object);   // true
  console.log(date.constructor === Date);     // true

  // 直接量也能直接点 constructor
  console.log([].constructor === arr.constructor);              // true
  console.log((function () {}).constructor === fn.constructor); // true
  // 系统创建的函数和自己创建的函数,构造函数都是 Function
  console.log(parseInt.constructor === fn.constructor);         // true
</script>
</body>
</html>

关键行与易错点

  • instanceof 是二元运算符,两个操作数:左边是对象,右边是构造函数;constructor 是每个对象都有的属性,系统自动添加,直接拿到该对象的构造函数,二者判断结果一致。
  • Object 是唯一的“特例”:任何对象 xxx instanceof Object 都为 true,因为 Object 只具备对象的最基本特征,其他类型都是在它基础上扩展的(原因要到原型链才讲得通)。但反过来 user instanceof Array 必须是 false,不要把这个特例套到别的构造函数上。
  • 判断相等用 ===,不要用 ==;比较的是“构造函数是不是同一个”,而不是“长得像不像”。
  • 常见错误:把 constructor 写成 constructor() 去调用,它是个属性,不是方法,直接点出来比较即可。

代码三:自定义构造函数与实例化 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>自定义构造函数</title>
</head>
<body>
<script>
  /* 自定义构造函数:本质就是一个普通函数,首字母大写只是为了和普通函数区分 */
  function User(uName, age, address) {
    // this 指向 User 未来的实例(new 出来的那个对象),不是 User 自己
    this.name    = uName;   // 形参名不必和属性名相同
    this.age     = age;
    this.address = address;

    // 方法:描述这种对象能干什么
    this.addShopCart = function (product) {
      console.log(this.name + '将' + product + '加入购物车');
    };
    this.buy = function (product) {
      console.log(this.name + '购买了' + product);
    };
  }

  /* 实例化:new 一个构造函数,就产生一个新对象 */
  var u1 = new User('高小乐', 18, '上海');
  var u2 = new User('曹操', 79, '北京');

  console.log(u1);                    // User { name: '高小乐', age: 18, … }
  console.log(u2);                    // User { name: '曹操',   age: 79, … }
  console.log(u1.name, u2.name);      // 高小乐 曹操(属性名相同,值各不相同)

  // 实例调用方法,方法内部的 this 指向调用它的那个实例
  u1.addShopCart('奶粉');             // 高小乐将奶粉加入购物车
  u2.addShopCart('大蒜');             // 曹操将大蒜加入购物车
  u2.buy('大蒜');                     // 曹操购买了大蒜

  // 判断构造函数
  console.log(u1 instanceof User);        // true
  console.log(u1.constructor === User);   // true

  // 同一构造函数的不同实例:长得一样,但互不相等(内存独立)
  console.log(u1 === u2);                 // false

  /* 字面量就是实例化的简写 */
  var a1 = new Array(), a2 = [];
  console.log(a1 === a2);                          // false
  console.log(a1.constructor === a2.constructor);  // true
  var o1 = new Object(), o2 = {};
  console.log(o1 === o2);                          // false
</script>
</body>
</html>

关键行与易错点

  • 构造函数就是普通函数,只要用 new 去调用它,它就是构造函数;首字母大写不是语法,只是约定俗成的区分方式(系统自带的 Array、Function、Object、Date 都是大写)。
  • this 在构造函数体内表示 User 未来要产生的那个实例,所以 this.name = ... 是给实例设置属性,而不是给 User 函数本身设置属性。
  • 形参名和属性名可以不同(这里用 uName 对 this.name),两者完全不冲突;但如果写成 this.name = '高小乐' 这种写死的值,那么所有实例的名字都一样,就失去了“描述一种类型”的意义,必须用形参传值。
  • new User(...) 每执行一次就产生一个全新的、内存独立的对象,所以 u1 === u2 一定是 false;对象类型判断相等不看长相,只看是不是同一个。
  • 构造函数体内可以写别的语句(new 时会执行),但规范做法只用来通过 this 给实例挂属性和方法,不要在里面乱写逻辑。

常见错误 ​

  1. 现象:用 for...in 遍历数组,结果混入自定义属性,误以为只输出元素。→ 原因:for...in 遍历可枚举属性,数组索引也是属性。→ 修法:遍历元素用 for/forEach/for...of;确需属性遍历时明确过滤条件。
  2. 现象:delete arr[1] 后以为长度变短、后续元素前移。→ 原因:delete 删属性,不重排索引,length 不变。→ 修法:删除数组元素用 splice;栈/队列操作分别用 pop/shift。
  3. 现象:typeof arr、typeof date、typeof 自定义实例 都得到 "object",无法区分具体类型。→ 原因:typeof 只粗分原始类型与 object/function。→ 修法:按场景用 Array.isArray、instanceof、constructor,不要只靠 typeof 判具体对象类型。
  4. 现象:自定义构造函数漏写 new 直接调用,属性挂到全局/undefined 报错。→ 原因:this 未绑定新实例。→ 修法:调用处补 new;必要时在构造函数内防御:if (!(this instanceof User)) return new User(...)。
  5. 现象:两个 new 出来的对象“看起来一样”,但 === 为 false,误判代码有错。→ 原因:每次实例化产生独立内存,引用比较看身份。→ 修法:需要内容比较时逐属性比较;需要同一对象则复用引用。
  6. 现象:构造函数内写死 this.name = '高小乐',所有实例同名。→ 原因:把实例差异写进共同描述。→ 修法:用形参接收差异并赋给 this 属性;共同结构留在构造函数。

练习 ​

  1. 创建 arr = [100, 200, 300],添加 owner 属性;分别用 for...in 和 forEach 输出,记录差异;再用 delete 与 splice 删除索引 1,对比 length 和元素结果。
  2. 定义构造函数 Product(name, price),实例化 p1、p2;验证并解释:p1 instanceof Product、p1.constructor === Product、p1 === p2。
  3. 定义构造函数 User(name, age),添加 addToCart(product) 方法;实例化两个用户并输出不同 name 下的方法结果;再写出漏写 new 时的现象与两种修复方案。
  4. 给定 arr、fn、obj、date 四个值,写代码输出它们的 constructor 与 instanceof 判断结果,至少覆盖 Array、Function、Object、Date,并标出 Object 特例。
  5. 写函数 ownEnumerableKeys(obj) 返回对象自有可枚举属性名;用普通对象和带自定义属性的数组各测一次,说明为什么不能直接把 for...in 结果当自有属性列表。

原视频对照 ​

#视频时长
1Object对象是最纯净的对象13:35
2什么是构造函数10:22
3对象和构造函数的关系05:35
4判断对象的构造函数08:37
5实例化07:09
6自定义构造函数21:23

小结 ​

  1. Object 类型对象是对象基本特征的基线;数组/函数可复用对象属性操作,但数组元素语义不能被 delete/for...in 替代。
  2. for...in 的语义是可枚举属性,不是“数组元素遍历器”;数组元素遍历应优先 for/forEach/for...of。
  3. delete 是删属性,不是删数组元素;数组增删改查应使用数组 API。
  4. 构造函数与数据类型对应;自定义构造函数就是自定义一种对象类型,实例共享结构、不共享身份。
  5. instanceof 判断“是否为某构造函数的实例”,constructor 读取“来源构造器”;类型判断优先语义明确 API,Object 的 instanceof 需单独看待。
  6. 每次 new 都产生独立对象,=== 比较引用身份;外形相同不等于相等。
  7. 自定义构造函数的核心分工:形参承载实例差异,this 挂载实例属性/方法,调用时必须按构造函数方式使用。
  8. 构造函数本质仍是函数;分析时区分“构造语境”和“函数/对象语境”,避免把两种身份混在一起。