Skip to content

认识对象类型

在数据类型中我们提到还有一种特别的类型:对象类型

  • 对象类型涉及到 JavaScript 的各个方面,所以掌握对象类型非常重要
  • 对象类型是一种存储键值对(key-value)的更复杂的数据类型

为什么需要对象类型呢?

基本数据类型可以存储一些简单的值,但是现实世界的事物抽象成程序时,往往比较复杂

比如一个人,有自己的特性(比如姓名、年龄、身高),有一些行为(比如跑步、学习、工作)

比如一辆车,有自己的特性(比如颜色、重量、速度),有一些行为(比如行驶)

这个时候,我们需要一种新的类型将这些特性和行为组织在一起,这种类型就是对象类型

  • 对象类型可以使用{…}来创建的复杂类型,里面包含的是键值对(“key: value”)
  • 键值对可以是属性和方法(在对象中的函数称之为方法)
  • 其中 key 是字符串(也叫做属性名 property name ,ES6 之后也可以是 Symbol 类型,后续学习
  • 其中 value 可以是任意类型,包括基本数据类型、函数类型、对象类型等
js
/*
      两个术语: 函数/方法
         函数(function): 如果在JavaScript代码中通过function默认定义一个结构, 称之为是函数.
         方法(method): 如果将一个函数放到对象中, 作为对象的一个属性, 那么将这个函数称之为方法.
*/
function foo() {}

// key: 字符串类型, 但是在定义对象的属性名时, 大部分情况下引号都是可以省略的
var person = {
  // key: value
  name: "why",
  age: 18,
  height: 1.88,
  "my friend": {
    name: "kobe",
    age: 30,
  },
  run: function () {
    console.log("running");
  },
  eat: function () {
    console.log("eat foods");
  },
  study: function () {
    console.log("studying");
  },
};

创建对象和使用对象

对象的创建方法有很多,包括三种:

  • 对象字面量(Object Literal):通过{}
  • new Object+动态添加属性
  • new 其他类
js
// 1.对象字面量
var obj1 = {
  name: "why",
};

// 2.new Object()
// // Object构造函数
var obj2 = new Object();
obj2.name = "kobe";

// // 3.new 其他类()
function Person() {}
var obj3 = new Person();

目前我们主要掌握对象字面量的方式,后续我们学习其他两种方式

属性之间是以逗号( comma )分割的

对象的使用过程包括如下操作:

  • 访问对象的属性
  • 修改对象的属性
  • 添加对象的属性
  • 删除对象的属性
js
// 1.定义了一个对象
var info = {
  name: "why",
  age: 18,
  friend: {
    name: "kobe",
    age: 30,
  },
  running: function () {
    console.log("running~");
  },
};

// 2.访问对象中的属性
console.log(info.name);
console.log(info.friend.name);
info.running();

// 3.修改对象中的属性
info.age = 25;
info.running = function () {
  alert("I am running~");
};
console.log(info.age);
info.running();

// 4.添加对象中的属性
info.height = 1.88;
info.studying = function () {
  console.log("I am studying~");
};
console.log(info);

// 5.删除对象中的属性
// delete关键字(操作符)
delete info.age;
delete info.height;
console.log(info);

方括号和引用的使用

为什么需要使用方括号呢?

对于多次属性来说,JavaScript 是无法理解的

js
// 无法理解的属性
info.good friend = "why"

这是因为点符号要求 key 是有效的变量标识符

  • 不包含空格,不以数字开头,也不包含特殊字符(允许使用 $ 和 _)

这个时候我们可以使用方括号:

方括号运行我们在定义或者操作属性时更加的灵活

js
var obj = {
  name: "why",
  "my friend": "kobe",
  "eating something": function () {
    console.log("eating~");
  },
};

console.log(obj["my friend"]);
console.log(obj.name);
console.log(obj["name"]);

// obj["eating something"]()
var eatKey = "eating something";
obj[eatKey]();

对象的练习

js
// 定义商品对象
var product = {
  name: "鞋子",
  desc: "鞋子非常棒!!!",
  price: 99,
  brand: "nike",
};

// 定义手机对象
var phone = {
  name: "iPhone 13 Pro Max",
  desc: "对iPhone的描述信息",
  price: 888,
  callPhone: function (phoneNum) {
    console.log("打电话给某人:", phoneNum);
  },
  playGame: function (gameName) {
    console.log("玩游戏:", gameName);
  },
};

// 定义用户对象
var user = {
  id: 1111111,
  account: "coderwhy",
  nickname: "coderwhy",
  password: "xxx123",
  avatarURL: "图片地址",
  role: {
    id: 110,
    name: "管理员",
    createTime: "2033-03-03",
  },
};

对象的遍历

对象的遍历(迭代):表示获取对象中所有的属性和方法

  • Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组

遍历方式一:普通 for 循环

遍历方式二:for in 遍历方法

js
var info = {
  name: "why",
  age: 18,
  height: 1.88,
};

// console.log(Object.keys(info))

// 对对象进行遍历
// 1.普通for循环
var infoKeys = Object.keys(info);
for (var i = 0; i < infoKeys.length; i++) {
  var key = infoKeys[i];
  var value = info[key];
  console.log(`key: ${key}, value: ${value}`);
}

// 2.for..in..: 遍历对象
for (var key in info) {
  var value = info[key];
  console.log(`key: ${key}, value: ${value}`);
}

// 对象不支持:  for..of..: 默认是不能遍历对象
// for (var foo of info) {
// }

栈内存和堆内存

我们知道程序是需要加载到内存中来执行的,我们可以将内存划分为两个区域:栈内存和堆内存

  • 原始类型占据的空间是在栈内存中分配的
  • 对象类型占据的空间是在堆内存中分配的

image-20221127231114018

值类型和引用类型

原始类型的保存方式:在变量中保存的是值本身,所以原始类型也被称之为值类型

对象类型的保存方式:在变量中保存的是对象的“引用”,所以对象类型也被称之为引用类型

思考下面的现象

js
var num1 = 123;
var num2 = 123;
console.log(num1 === num2); // true

// 1.现象一: 两个对象的比较
var obj1 = {};
var obj2 = {};
console.log(obj1 === obj2); // false

它们在内存中的表现是下面这样,两个对象的内存地址不一样,所以返回 false

image-20221127231511209

js
// 2.现象二: 引用的赋值
var info = {
  name: "why",
  friend: {
    name: "kobe",
  },
};

var friend = info.friend;
friend.name = "james";
console.log(info.friend.name); // james

上面的代码在内存中的表现如下:

image-20221127232111081

js
// 3.现象三: 值传递
function foo(a) {
  a = 200;
}
var num = 100;
foo(num);
console.log(num); // 100

执行 foo(num),将 num 的值赋值给 a,自始至终 num 都没有改变

image-20221127233702664

js
// 4.现象四: 引用传递, 但是在函数中创建了一个新对象, 没有对传入对象进行修改
function foo(a) {
  a = {
    name: "why",
  };
}
var obj = {
  name: "obj",
};
foo(obj);
console.log(obj); // { name: "obj" }

image-20221127234706798

js
// 5.现象五: 引用传递, 但是对传入的对象进行修改
function foo(a) {
  a.name = "why";
}

var obj = {
  name: "obj",
};
foo(obj);
console.log(obj); // { name: "why" }

image-20221127235258962

为什么需要 this?

在常见的编程语言中,几乎都有 this 这个关键字(Objective-C 中使用的是 self),但是 JavaScript 中的 this 和常见的面向对象 语言中的 this 不太一样:

  • 常见面向对象的编程语言中,比如 Java、C++、Swift、Dart 等等一系列语言中,this 通常只会出现在类的方法中
  • 也就是你需要有一个类,类中的方法(特别是实例方法)中,this 代表的是当前调用对象

但是 JavaScript 中的 this 更加灵活,无论是它出现的位置还是它代表的含义

我们来看一下编写一个 obj 的对象,有 this 和没有 this 的区别:

有 this 的情况

js
var info = {
  name: "why",
  age: 18,
  running: function () {
    console.log("running~", this.name);
  },
  eating: function () {
    console.log("eating~", this.name);
  },
  studying: function () {
    console.log("studying~", this.name);
  },
};

info.running();
info.eating();
info.studying();

没有 this 的情况

js
var info = {
  name: "why",
  age: 18,
  running: function () {
    console.log("running~", info.name);
  },
  eating: function () {
    console.log("eating~", info.name);
  },
  studying: function () {
    console.log("studying~", info.name);
  },
};

info.running();
info.eating();
info.studying();

this 指向什么?

目前掌握两个 this 的判断方法:

  • 在全局环境下面,this 指向 window
js
// 函数中是有一个this的变量, this变量在大多数情况下会指向一个对象
// arguments保存的是传入的所有参数

// 情况一: 如果普通的函数被默认调用, 那么this指向的就是window
function foo(name, age) {
  console.log(arguments);
  console.log(this); // window
}
foo("abc", 123);
  • 通过对象调用,this 指向调用的对象
js
// 情况二: 如果函数它是被某一个对象来引用并且调用它, 那么this会指向这个对象(调用的那个调用)
var obj = {
  name: "why",
  running: function () {
    console.log(this); // obj
    console.log(obj);
    console.log(this === obj); // true
  },
};
obj.running();

考验题目

题目一:

js
var obj = {
  name: "why",
  running: function () {
    console.log(this); // window
  },
};

var fn = obj.running;
fn();

题目二:

js
function bar() {
  console.log(this); // obj对象
}
var obj = {
  name: "why",
  bar: bar,
};
obj.bar();

后续我们还会学习其他,也会给大家总结 this 的规律

类和对象的思维方式

我们来思考一个问题:如果需要在开发中创建一系列的相似的对象,我们应该如何操作呢?

比如下面的例子:

  • 游戏中创建一系列的英雄(英雄具备的特性是相似的,比如都有名字、技能、价格,但是具体的值又不相同)
  • 学生系统中创建一系列的学生(学生都有学号、姓名、年龄等,但是具体的值又不相同)

当然,一种办法是我们创建一系列的对象:

js
// 一系列的学生对象
// 重复代码的复用: for/函数
var stu1 = {
  name: "why",
  age: 18,
  height: 1.88,
  running: function () {
    console.log("running~");
  },
};
var stu2 = {
  name: "kobe",
  age: 30,
  height: 1.98,
  running: function () {
    console.log("running~");
  },
};
var stu3 = {
  name: "james",
  age: 25,
  height: 2.05,
  running: function () {
    console.log("running~");
  },
};

这种方式有一个很大的弊端:创建同样的对象时,需要编写重复的代码

我们是否有可以批量创建对象,但是又让它们的属性不一样呢?

创建对象的方案 – 工厂函数

我们可以想到的一种创建对象的方式:工厂函数

  • 我们可以封装一个函数,这个函数用于帮助我们创建一个对象,我们只需要重复调用这个函数即可
  • 工厂模式其实是一种常见的设计模式
js
// 工厂函数(工厂生产student对象) -> 一种设计模式
// 通过工厂设计模式, 自己来定义了一个这样的函数
function createStudent(name, age, height) {
  var stu = {};
  stu.name = name;
  stu.age = age;
  stu.height = height;
  stu.running = function () {
    console.log("running~");
  };
  return stu;
}

var stu1 = createStudent("why", 18, 1.88);
var stu2 = createStudent("kobe", 30, 1.98);
var stu3 = createStudent("james", 25, 2.05);
console.log(stu1); // Object
console.log(stu2); // Object
console.log(stu3); // Object

认识构造函数

工厂方法创建对象有一个比较大的问题:我们在打印对象时,对象的类型都是 Object 类型

  • 但是从某些角度来说,这些对象应该有一个他们共同的类型
  • 下面我们来看一下另外一种模式:构造函数的方式

我们先理解什么是构造函数?

  • 构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数
  • 在其他面向的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法
  • 但是 JavaScript 中的构造函数有点不太一样,构造函数扮演了其他语言中类的角色

也就是在 JavaScript 中,构造函数其实就是类的扮演者:

  • 比如系统默认给我们提供的 Date 就是一个构造函数,也可以看成是一个类
  • 在 ES5 之前,我们都是通过 function 来声明一个构造函数(类)的,之后通过 new 关键字来对其进行调用
  • 在 ES6 之后,JavaScript 可以像别的语言一样,通过 class 来声明一个类

那么类和对象到底是什么关系呢?

类和对象的关系

那么什么是类(构造函数)呢?

  • 现实生活中往往是根据一份描述/一个模板来创建一个实体对象的
  • 编程语言也是一样, 也必须先有一份描述, 在这份描述中说明将来创建出来的对象有哪些属性(成员变量)和行为(成员方法)

比如现实生活中,我们会如此来描述一些事物:

  • 比如水果 fruits 是一类事物的统称,苹果、橘子、葡萄等是具体的对象
  • 比如人 person 是一类事物的统称,而 Jim、Lucy、Lily、李雷、韩梅梅是具体的对象

JavaScript 中的类(ES5)

我们前面说过,在 JavaScript 中类的表示形式就是构造函数

JavaScript 中的构造函数是怎么样的?

  • 构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别
  • 那么如果这么一个普通的函数被使用 new 操作符来调用了,那么这个函数就称之为是一个构造函数

如果一个函数被使用 new 操作符调用了,那么它会执行如下操作:

  • 在内存中创建一个新的对象(空对象)
  • 这个对象内部的[[prototype]]属性会被赋值为该构造函数的 prototype 属性;(后面详细讲)
  • 构造函数内部的 this,会指向创建出来的新对象
  • 执行函数的内部代码(函数体代码)
  • 如果构造函数没有返回非空对象,则返回创建出来的新对象

接下来,我们可以用构造函数的方式来实现一下批量创建学生

创建对象的方案 – 构造函数(类)

我们来通过构造函数实现一下:

js
// JavaScript已经默认提供给了我们可以更加符合JavaScript思维方式(面向对象的思维方式)的一种创建对象的规则
// 在函数中的this一般指向某一个对象
/*
      如果一个函数被new操作符调用
        1.创建出来一个新的空对象
        2.让this指向这个空对象
        3.执行函数体的代码块
        4.如果没有明确的返回一个非空对象, 那么this指向的对象会自动返回
*/
function coder(name, age, height) {
  this.name = name;
  this.age = age;
  this.height = height;
  this.running = function () {
    console.log("running~");
  };
}

// 在函数调用的前面加 new 关键字(操作符)
var stu1 = new coder("why", 18, 1.88);
var stu2 = new coder("kobe", 30, 1.98);
console.log(stu1, stu2);

这个构造函数可以确保我们的对象是有 coder 的类型的(实际是 constructor 的属性,这个我们后续再探讨)

事实上构造函数还有很多其他的特性:

  • 比如原型、原型链、实现继承的方案
  • 比如 ES6 中类、继承的实现

额外补充

构造函数的补充

js
// 创建一系列的对象
// 构造函数的名称: 使用大驼峰
function Person() {}

var p1 = new Person();
console.log(p1);

// 平时创建普通的对象
// new Object()
var obj1 = {};
var obj2 = new Object();
var obj3 = new Person();

// 普通函数: 使用小驼峰
function sayHello() {}

全局对象 window

js
// 浏览器中存在一个全局对象object -> window
// 作用一: 查找变量时, 最终会找到window头上
// 作用二: 将一些浏览器全局提供给我们的变量/函数/对象, 放在window对象上面
// 作用三(了解): 使用var定义的变量会被默认添加到window上面
console.log(window);

// 使用var定义变量
var message = "Hello World";

function foo() {
  // 自己的作用域
  // abc()
  // alert("Hello World")
  console.log(window.console === console); // true

  // 创建一个对象
  // var obj = new Object()
  console.log(window.Object === Object); // true

  // DOM
  console.log(document);

  // window.message
  console.log(window.message); // Hello World
}
foo();

函数也是对象

js
// 定义原始类型的变量
var name = "why";
var age = 18;

// 定义对象类型的变量
// 地址 - 指针 - 引用
var obj = {}; // 堆内存
var foo = function () {}; // 堆内存
function bar() {} // 堆内存

console.log(typeof obj); // object
console.log(typeof foo); // function -> object

// var stu = new Student() // stu是一个Student -> Person

// 引申一些别的知识(了解)
var info = {};
info.name = "abc";

function sayHello() {}
sayHello.age = 18;
console.log(sayHello.age); // 18

function Dog() {}
// 构造函数上(类上面)添加的函数, 称之为类方法
Dog.running = function () {};
Dog.running();