JavaScript 中的对象(一)- 对象的概念、模型、以及创建

导言

众所周知,JavaScript 当中的面向对象机制和其他的语言很不一样,在 ES5 之前,JavaScript 中并没有类这个概念,函数充当了类的角色,另外就是 JavaScript 中的对象用起来更像是哈希一类的数据结构,但是它也是可以实现我们经常说的面向对象四要素,也就是抽象、封装、继承、多态。在接下来的话题中,我用两篇的篇幅介绍下 JavaScript 中的面向对象机制


概览

上面的这张图是这篇文章的知识地图,声明一下,写这篇文章也是在看了 《Professional: JavaScript for Web Developers》 这本书的相关章节,有些人可能会认为这些内容过时了,现在 ES8 都出了,我们用的都是更先进的语法和工具,没人再会考虑你图中画出来的东西,的确,但是在这里我只想说, 学一门语言也好,学一门技术也好,我们重点是要掌握其设计的核心思想,以及演变的原因,我上面列出来的各种模式其实是有相互联系的, 当前辈们发现一种模式不能更好的帮助我们解决我们需要解决的问题的时候,另外一种模式随之诞生,技术的发展是不断迭代的,想要对技术有更深层次的渗透和领悟,最好的方式就是多了解我们现在用的技术是怎么演变过来的,不然你的知识就只会是空中楼阁,随时会坍塌。再者说来 JavaScript 版本其实是向下兼容的,这些知识虽然老旧了些,但是你依然可以把其运用在实际的开发当中。


对象的特性

JavaScript 中的对象有点像 Java 中的 HashMap,Python 中的 dict,其实就是一个 key, value 的集合,这个 value 比较广,既可以是其他的对象,也可以是函数,你可以看到它其实非常的灵活,声明并创建一个对象的方式其实非常的简单,如下:

const person = {
  name: "小明",
  age: 25,
  job: "software engineer",
  sayName: function() {
    console.log(this.name);
  }
};

person.sayName(); // 小明
复制代码

你可以看到,获得一个对象我们可以不需要类,甚至不需要构造函数,但是仔细想想,这样真的好吗?首先每个变量其实没有所谓的访问权限,谁都可以任意更改这里面的值,这么看并不好设计和管理。于是,JavaScript 中就给对象添加了一些特性,分别是数据特性访问特性

数据特性有四个,分别是 configurable、enumerable、writable、value,访问特性中也有四个,分别是 configurable、enumerable、get、set,相信你不难理解这些特性分别代表什么意思,这里就不再赘述,你可以通过 Object.defineProperty() 函数去修改这些特性,有了这些特性之后,我们可以修改对象当中的成员的访问权限等,对象就不再是之前那个常规的哈希一类的数据结构了,它变的更加容易管理。


工厂模式

前面讲到了 JavaScript 的对象,有了对象是不是就够了呢?当然不是,虽然我们可以不通过类和一些模版来创建对象,但是这种的直接创建是非常不方便的,如果对象里面的东西很多,那么每次都得找到之前创建过的对象,进行大量的复制粘贴,代码冗余,而且特别难懂,所以我们还是需要一个机制来帮助我们创建对象,函数是 JavaScript 中的一等公民,当然最初的想法就是借助函数来帮我们创建对象:

function createPerson(name, age, job) {
  let o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function() {
    console.log(this.name);
  };
  return o;
}

const person = createPerson("小明", 25, "software engineer");
person.sayName(); // 小明
复制代码

相比之前来说,构建对象确实方便了不少,我们只需要调用函数,然后传递相应的参数进去,满足我们需求的对象就会借由函数产生。


构造器模式

上面提到了使用函数去创建对象,但是这里还是有一个问题就是我们怎么知道对象的类型呢? 对象其实是需要多样化的,比如说是 Array、String、Number 等等,这些都是不同种类的对象,可能有些人会说我们通过函数名来区分,不同的函数名产生不同类型的对象,这当然是一种解决办法,但是问题是这样做真的好吗?首先这样做会使得函数的涵盖面太广了,普通函数是函数,对象的构造器也是函数,我们如何区分?于是我们有了构造器函数:

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

const person = new Person("小明", 25, "software engineer");
person.sayName(); // 小明
复制代码

这里和之前的工厂模式有几点不同:

  • 函数本身就是一个类型,函数内部不需要显式地去创建对象,函数名的开头字母从之前的小写变成了大写,用来区分普通函数和构造器函数
  • 构造器函数不需要有返回值,而且使用 this 关键字来表示当前的对象
  • 使用构造器函数创建对象的时候,我们使用的是 new 关键字,而不是直接调用函数

你可以看到的是这里的函数充当的就是 class,类的角色,这里还有两个特别有意思的调用方式:

Person("小明", 25, "software engineer"); 
window.sayName();

const o = new Object();
Person.call(o, "小明", 25, "software engineer");
o.sayName();
复制代码

首先说说第一种调用方式,如果我们使用普通函数的调用方式去调用构造函数,那么 this 关键字是没有指明具体的对象的,JavaScript 中会默认此时 this 指向全局变量 window。第二种方式是把这些值加在一个对象上,也就是构造函数会通过 call() 函数把 o 对象的 this 给带进来,这样函数中的基于 this 的操作都是针对 o 对象了。


原型模式

有了构造函数,事情看上去已经很圆满了。但是这里还是有个问题,如果你仔细想想就知道,函数里面是没有静态成员的,这样的话每个对象都会将构造函数里面的东西重新复制一份,就比如之前的 sayName() 函数,这个函数功能很简单,所有对象公用一个就够了,复制那么多份完全没有必要,耗费了大量的资源。

原型,prototype 其实是学习 JavaScript 的一个难点,个人认为其设计的初衷其实是为了帮助资源共享,所有的同一类型的对象可以通过原型来访问共有的属性:

function Person() {}
Person.prototype.name = "小明";
Person.prototype.age = 25;
Person.prototype.job = "software engineer";
Person.prototype.sayName = function() {console.log(this.name)}

const person1 = new Person();
const person2 = new Person();

person1.sayName(); // “小明”
person2.sayName(); // “小明”

console.log(person1.sayName === person2.sayName); // true
复制代码

你可以看到我们并没有在构造函数里面定义任何的变量,我们在函数的外部通过原型来赋值,但是通过构造函数创建出来的对象还是可以访问到我们在原型上赋的值,并且你也可以发现不同的对象其实访问同一个原型,那么说来函数的原型只有一个,对象可以有很多个。构造函数、原型、对象的关系可以表示成下图:

这里对象里面都有一个指针指向原型,原型里面会有对应构造器指向相应的构造函数,构造函数里面也有其原型。当我们查找对象当中的一个成员,如果对象里面没有,它就会去到它的原型里面去找,我们可以使用原型当中的 isPrototypeOf() 函数去判断一个对象的原型是不是一个构造函数的原型:

const o = new Object();
const person = new Person();

console.log(Person.prototype.isPrototyeOf(o)); // false
console.log(Person.prototype.isPrototyeOf(person)); // true
复制代码

但是这里还是有一个问题就是,现在我们可以获取到对象当中的属性,但是我们并不清楚这个属性是对象当中的还是原型当中的,好在 JavaScript 中有相应的机制帮助我们解决这个困惑,hasOwnProperty() 函数可以帮助我们确认一个对象当中有无此属性,另外就是 prop in object 这个语句是只要对象可以获取该成员,不管其是在原型中还是在对象中,我们都返回 true,于是我们可以借助这两个特性来判断属性是在原型中还是在对象中:

const person = new Person();
person.hasOwnProperty("name"); // 属性否在对象中
console.log(!hasOwnProperty() && (name in person)); // 属性不在对象而在原型中
复制代码

另外有一点需要注意的就是,如果我们直接给原型整体,而不是原型当中某个变量赋值,这会导致之前的原型被覆盖,也就是我们新创建了一个原型,这样的话新创建的对象会和之前的对象脱节。


混合模式

前面讲到的原型模式其实也有它的缺点,就是所有的对象共用同一个原型,这样原型中所有的数据都是 shared,那也就是说之前的对象改变了原型当中的变量的值,那么新创建的对象也会受到影响。

原型模式其实是构造器模式的对立,一个 share 所有的资源,另外一个是所有的资源都是分隔开来的,如果我们将两种模式融合一下,将需要公有的成员公有化,需要私有的成员私有化,那么就有了混合模式:

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
}

Person.prototype = {
  construtor: Person,
  sayName: function() { console.log(this.name); }
}
复制代码

这个创建对象的方式也是实际当中用的最多的,当然其本身并没有什么特别的地方,只是结合了之前的两种构造方式的优点。

当然还有其他的一些构造方式,比如说动态的原型模式、稳健构造器模式、寄生构造模式,其实这些模式也都是基于构造器模式和原型模式演变的,他们也没有什么特别的地方,只是为了适应某种实际应用的需要而生成的,这里就不做过多的叙述。


总结

回过头去看一开始的那张图就觉得特别有意思,知识不再是一个个的点,而是相互之间有关联的。最初我们只有对象这个概念,但是这样不方便管理,于是我们给对象添加了一些特性,类似访问权限之类的约束条件;随后我们发现创建对象仅使用简单的赋值法并不方便,于是我们需要借助函数,一开始是工厂模式,但是它不够通用化,也不能简单直观地反应出对象的类型,于是就有了构造器模式,但是构造器模式下创建对象会对所有成员都进行一次复制,这其实是没有必要的,同一类的对象也是需要共享资源的,就像 Java 中的静态成员变量一样,JavaScript 中的对象也需要一个所有同类对象都能共享资源的方式,于是原型出来了,但是貌似原型只能是共享,还是和构造函数做一个结合吧,需要共享的共享,不需要共享的单独创建,于是我们有了最终的混合模式。

知识或者是技术的演变就是这样,层层迭代,最终才到了我们现在所使用的技术,盲目地追求新技术其实会让你知其然,不知其所以然,因此,我们有时还是需要思考技术出现的原因和背景。好了,对象的创建就讲到这,但是这当然还没有完,面向对象当中的四要素我们只说了其中的抽象和封装,还有继承和多态,这个留在下次讲,可以思考下在没有类的前提下如何实现继承? 这其实挺有意思的。

转载于:https://juejin.im/post/5d1787965188257da034d3b7

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值