JavaScript 中的面向对象

472 阅读9分钟

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

到底什么是面向对象编程

面向对象 是软件程序设计开发的一种方法,一种编程范式,其本质是以建立模型体现出来的抽象思维过程和面向对象的方法。模型是用来反映现实世界中事物特征的。是对事物特征和变化规律的一种抽象,通过建立模型而达到的抽象是人们对客体认识的深化。

什么是对象?

对象是对概念的具体化体现:

一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。

当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。

编程中对象是一个容器,封装了属性(property)和方法(method)

属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把 "动物" 进行抽象,使用“属性”记录具体的特征,比如“大小”、“形态”、“毛色”;使用“方法”表示动物的某种行为,比如 “奔跑”、“捕猎”、“跳跃。

举个小例子:

特征 (属性):满身猴毛,尖嘴竖耳,黄发金箍;

行为 (方法):耍金箍棒,踏筋斗云,上凌霄殿;

你想到的不是孙悟空就是六耳猕猴,为什么你脑海里能出现一个画面,是因为我通过属性和方法描述了一个 “类”,而你的脑海里便出现了通过类具体生成的 “对象” ;

ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数
严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都
映射到一个值。也可以将其简单理解为:数据集或功能集

面向对象编程有三大特性:封装性、继承性、多态性,这三大特性你先有个印象就行,这需要我们学习完具体的语法规则之后,有了一定的使用经验才能再思考得出结论;

avaScript 中的面向对象

JavaScript 语言本身的设计缺陷,误打误撞,成了解释最为彻底的“世界原本的样子”的计算机编程语言;

-- 西岭《凡人凡语》

Everything is object (万物皆对象),JS 语言中将一切都视为 对象

JavaScript 语言的对象体系,不基于“类” 创建对象,是基于构造函数(constructor)和原型链(prototype)。

简单方式创建对象

我们可以直接通过 new Object() 创建:

var person = new Object()
person.name = 'Jack'
person.age = 18

person.sayName = function () {
    console.log(this.name)
}

字面量方式创建对象

每次创建通过 new Object() 比较麻烦,所以可以通过它的简写形式对象字面量来创建:

var person = {
  name: 'Jack',
  age: 18,
  sayName: function () {
    console.log(this.name)
  }
}

构造函数

JavaScript 语言使用构造函数作为对象的模板。
所谓 "构造函数",就是一个普通的函数,只不过我们专门用它来生成对象,这样使用的函数,就是构造函数
它提供模板,描述对象的基本结构。
一个构造函数,可以生成多个对象,这些对象都有相同的结构。

function Person (name, age) {
  this.name = name
  this.age = age
  this.sayName = function () {
    console.log(this.name)
  }
}

var p1 = new Person('Jack', 18)
p1.sayName() // => Jack

var p2 = new Person('Mike', 23)
p2.sayName() // => Mike

解析 构造函数代码 的执行

在上面的示例中,使用 new 操作符创建 Person 实例对象;

以这种方式调用构造函数会经历以下 5 个步骤:

  1. 创建一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的原型,指向构造函数的prototype属性。先记住,后面讲
  1. 将这个空对象赋值给函数内部的this关键字。
  2. 执行构造函数内部的代码。
  1. 返回新对象 (this)
function Person (name, age) {
  // 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象
  // 然后让内部的 this 指向新创建的对象
  // 接下来所有针对 this 的操作实际上操作的就是刚创建的这个对象

  this.name = name
  this.age = age
  this.sayName = function () {
    console.log(this.name)
  }

  // 在函数的结尾处会将 this 返回,也就是这个新对象
}

构造函数和实例对象的关系

构造函数是根据具体的事物抽象出来的抽象模板,实例对象是根据抽象的构造函数模板得到的具体实例对象;

实例对象由构造函数而来,一个构造函数可以生成很多具体的实例对象,而每个实例对象都是独一无二的;

每个对象都有一个 constructor 属性,该属性指向创建该实例的构造函数

反推出来,每一个对象都有其构造函数

console.log(p1.constructor === Person) // => true
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true

因此,我们可以通过实例对象的 constructor 属性判断实例和构造函数之间的关系;

构造函数存在的问题

以构造函数为模板,创建对象,对象的属性和方法都可以在构造函数内部定义;

function Cat(name, color) {
  this.name = name;
  this.color = color;
  this.say = function () {
    console.log('hello'+this.name,this.color);
  };
}
var cat1 = new Cat('猫', '白色'); 
var cat2 = new Cat('猫', '黑色'); 
cat1.say();
cat2.say();

在该示例中,从表面上看好像没什么问题,但是实际上这样做,有一个很大的弊端。
那就是对于每一个实例对象, namesay 都是一模一样的内容,
每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。

那么,能不能将相同的内容,放到公共部分,节约计算机资源呢?

原型

JavaScript 的每个对象都会继承一个父级对象,父级对象称为 原型 (prototype) 对象。

原型也是一个对象,原型对象上的所有属性和方法,都能被子对象 (派生对象) 共享,通过构造函数生成实例对象时,会自动为实例对象分配原型对象。 而每一个构造函数都有一个prototype属性,这个属性就是实例对象的原型对象

null 没有自己的原型对象。

这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在构造函数的 prototype 属性上,

也就是实例对象的原型对象上。

function Cat(color) {
  this.color = color;
}

Cat.prototype.name = "猫";
Cat.prototype.sayhello = function(){
    console.log('hello'+this.name,this.color);
}
Cat.prototype.saycolor = function (){
    console.log('hello'+this.color);
}

var cat1 = new Cat('白色'); 
var cat2 = new Cat('黑色'); 
cat1.sayhello();
cat2.saycolor();

这时所有实例对象的 name 属性和 sayhello()saycolor 方法,其实都是在同一个内存地址的对象中,也就是构造函数的 prototype 属性上,因此就提高了运行效率节省了内存空间。

原型及原型链

构造函数的 prototyp 属性,就是由这个构造函数 new 出来的所有实例对象的 原型对象

所有对象都有原型对象;

function Cat(name, color) {
    this.name = name;
 }

var cat1 = new Cat('猫');

console.log(cat1.__proto__.__proto__.__proto__);

而原型对象中的属性和方法,都可以被实例对象直接使用;

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性

  • 搜索首先从对象实例本身开始
  • 如果在实例中找到了具有给定名字的属性,则返回该属性的值
  • 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
  • 如果在原型对象中找到了这个属性,则返回该属性的值
  • 如果还是找不到,就到原型的原型去找,依次类推。
  • 如果直到最顶层的Object.prototype还是找不到,则返回undefined。

而这正是多个对象实例共享原型所保存的属性和方法的基本原理。

对象的属性和方法,有可能是定义在自身内,也有可能是定义在它的原型对象上。
由于原型本身也是对象,又有自己的原型,所以形成了一条可向上追溯的链条,叫 原型链(prototype chain)。

注意,不在要原型上形成多层链式查找,非常浪费资源

内置标准库与包装对象

内置标准对象中,对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”(wrapper)。

所谓“包装对象”,就是分别与数值、字符串、布尔值相对应的NumberStringBoolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。

var v1 = new Number(123);
var v2 = new String('abc');
var v3 = new Boolean(true);

typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"

v1 === 123 // false
v2 === 'abc' // false
v3 === true // false

包装对象的最大目的,首先是使得 JavaScript 的对象涵盖所有的值,其次使得原始类型的值可以方便地调用某些方法。

原始类型的值,可以自动当作对象调用,即调用各种对象的方法和参数。

这时,JavaScript 引擎会自动将原始类型的值转为包装对象实例,在使用后立刻销毁实例。

比如,字符串可以调用length属性,返回字符串的长度。

'abc'.length // 3

上面代码中,abc是一个字符串,本身不是对象,不能调用length属性。JavaScript 引擎自动将其转为包装对象,在这个对象上调用length属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换。