什么是封装
封装是面向对象的三个基本特征之一,将现实世界的事物抽象成计算机领域中的对象,对象同时具有属性和方法,我们在对象内部实现对象的各种逻辑程序,调用这个对象就只需要知道提供的属性和方法如何使用即可,里面的逻辑可以不必知道,这种抽象就是封装。所有面向对象的编程语言都支持封装,JavaScript灵活性很强,我们也可以实现封装。
我们在实际开发中,通常会将具有相同处理逻辑的代码进行函数的封装来减少代码的冗余,使代码变得简洁优雅。当多个实例对象拥有的一些属性和方法相同时,自然也可以将这些相同的属性和方法抽象出来,减少代码冗余。
虽然 Javascript 语言是一种基于对象的语言,在 JavaScript 中几乎一切皆对象。但是它和其他的面向对象编程语言还有一个不同,就是 JavaScript 的语法中没有类,那么我们如果要想将属性和方法,封装成一个对象,或者从原型对象生成一个实例对象,要怎么做?
原始模式的封装
假设现在有一个 Student 对象,这个对象中有编号、姓名、年龄等属性,如下所示:
var Student = {
id:1,
name:'长清',
age:18
}
如果我们要对这个对象进行封装,需要创建实例对象,然后将属性封装在一个对象里面:
var stu = {};
stu.id = 2;
stu.name = '夜华'
console.log(stu.id); // 输出:2
console.log(stu.name); // 输出:夜华
console.log(stu.age); // 输出:20
这样的封装有两个不足,一个就是如果我们生成多个实例,写起来会很麻烦。还有就是实例与原型之间,没有任何办法看出它们之间的联系。
改进原始模式
为了解决代码重复问题,我们可以使用写一个函数:
function Student(id, name, age){
return {
id:1,
name:'长清',
age:18
}
}
然后创建这个函数的实例对象,创建实例对象就相当于是在调用函数:
var stu1 = Student(2, '侠客', 33);
var stu2 = Student(3, '大侠', 24);
但是这个方法也有一个问题,实例对象之间依然没有内在的联系,不能反映出它们是同一个原型对象的实例。
构造函数模式封装
为了解决从原型对象生成实例的问题,Javascript 提供了一个构造函数模式。之前我们也讲到过,构造函数其实就是普通的函数,但是内部使用了 this
变量。对构造函数使用 new
运算符,就能生成实例,并且 this
变量会绑定在实例对象上。
示例:
将 Student 对象改用构造函数的方式来写,如下所示:
function Student(id, name, age){
this.id = id;
this.name = name;
this.age = age;
}
这样我们可以通过 new
关键字创建实例对象:
function Student(id, name, age){
this.id = id;
this.name = name;
this.age = age;
}
var stu1 = new Student(2, '侠客', 33);
var stu2 = new Student(3, '大侠', 24);
console.log(stu1.name); // 输出:侠客
console.log(stu2.name); // 输出:大侠
此时的实例对象 stu1
、stu2
都有一个 constructor
属性,指向它们的构造函数 Student
。
console.log(stu1.constructor); // 输出:[Function: Student]
console.log(stu2.constructor); // 输出:[Function: Student]
为了验证原型对象与实例对象之间的关系,JavaScript 中还提供了一个运算符 instanceof
:
console.log(stu1 instanceof Student); // 输出:true
console.log(stu2 instanceof Student); // 输出:true
通过构造函数的方式封装虽然好用,但是它也有一个问题,就是当构造函数中有不变的属性值和方法的时候,每次创建实例对象,这些属性值和方法都会一模一样,每生成一个实例都会生成重复的内容,这样虽然使用起来比较方便,不需要我们重复创建相同的属性或方法,但是也会多占一些内容。
所以我们想,可不可以让这些需要重复用到的属性和方法在内存中只生成一次,然后所有的实例都指向那个内存地址呢,这样不就可以提高效率了嘛。这就要用到 Prototype 模式。
Prototype模式
Javascript 中每一个构造函数都有一个 prototype
属性指向另一个对象。这个对象中的所有属性和方法,都会被构造函数的实例继承。所以我们可以将不会改变的属性和方法直接定义到 prototype
对象上。
示例:
例如 Student 构造函数中的 age 属性值都为 6,还有一个复用的方法 show,用来显示学生的信息:
function Student(id, name, age){
this.id = id;
this.name = name;
}
// 不变的属性值
Student.prototype.age = 6;
Student.prototype.show = function(){
console.log("我的名字是:" + this.name);
}
// 创建实例
var stu1 = new Student(2, '侠客', 33);
var stu2 = new Student(3, '大侠', 24);
stu1.show(); // 输出:我的名字是:侠客
console.log(stu1.age); // 输出:6
console.log(stu2.age); // 输出:6
此时不管你创建多少个实例对象,其中的 age
属性和 show
方法都在同一个内存地址,指向 prototype
对象。
我们可以来验证一个:
console.log(stu1.age == stu2.age); // 输出:true
console.log(stu1.show == stu2.show); // 输出:true
从输出结果可以看出,实例对象 stu1
和 stu2
中的属性和方法在同一内存中。但是这就是针对定义在 prototype
对象上的属性和方法来说,其他的属性和方法并不在同一内存中:
console.log(stu1.id == stu2.id); // 输出:false
console.log(stu1.name == stu2.name); // 输出:false