JS 对象式编程

前言

JS作为函数式编程的语言,受其他语言的影响,也可以用对象式编程,一种是用函数模仿对象,另一种是ES6添加的class。

对象字面量

JS中创建对象最原始的方式有两种:

方式一 对象字面量

1
2
3
4
5
6
7
var person = {
name: "leon",
age: "20",
greeting: function () {
alert('Hi!');
}
}

方式二 为Object实例添加属性方法

1
2
3
4
5
6
var person = new Object();
person.name = "leon";
person.age = "20";
person.greeting = function () {
alert('Hi!');
};
  • 优点:代码简单
  • 缺点: 创建多个对象会产生大量的代码,编写麻烦,且并没有实例与原型的概念。

解决办法:工厂模式。

工厂模式

工厂模式是编程领域一种广为人知的设计模式,它抽象了创建具体对象的过程。JS 中创建一个函数,把创建新对象、添加对象属性、返回对象的过程放到这个函数中,用户只需调用函数来生成对象而无需关注对象创建细节,这叫工厂模式:

1
2
3
4
5
6
7
8
9
10
11
12
function createPerson(name, age) {
var person = new Object();
person.name = name;
person.age = age;

person.greeting = function() {
alert('Hi!');
};
return person;
}

var person1 = createPerson("leon", "20");

优缺点

  • 优点:工厂模式解决了对象字面量创建对象代码重复问题,创建相似对象可以使用同一API。
  • 缺点:因为是调用函创建对象,无法识别对象的类型。

解决办法:构造函数

构造函数

JS 中构造函数与其他函数的唯一区别,就在于调用它的方式不同。任何函数,只要通过new 操作符来调用,那它就可以作为构造函数。

来看下面的例子:

1
2
3
4
5
6
7
8
9
function Person(name, age) {
this.name = name;
this.age = age;
this.greeting = function () {
alert('Hi!');
};
}
var person1 = new Person("leon", "20");
var person2 = new Person("jack", "21");

通过构造函数new一个实例经历了四步:

  1. 创建一个新对象;
  2. 将构造函数内的this绑定到新对象上;
  3. 为新对象添加属性和方法;
  4. 返回新对象(JS 引擎会默认添加 return this;)。

而通过构造函数创建的对象都有一个constructor属性,它是一个指向构造函数本身的指针,因此就可以检测对象的类型啦。:

1
2
alert(person1.constructor === Person) //true
alert(person1 instanceof Person) // true

但是仍然存在问题:

1
alert(person1.greeting == person2.greeting) //false

同一个构造函数中定义了greeting(),而不同实例上的同名函数却是不相等的,意味着这两个同名函数的内存空间不一致,也就是构造函数中的方法要在每个实例上重新创建一次。这显然是不划算的。

优缺点

  • 优点:解决了类似对象创建问题,且可以检测对象类型。
  • 缺点:构造函数方法要在每个实例上新建一次。

解决办法:原型模式。

原型模式

终于讲到了原型模式,JS 中每个构造函数都有一个prototype属性,这个属性是一个指针,指向原型对象,而这个原型对象包含了这个构造函数所有实例共享的属性和方法。

而实例对象中有一个proto属性,它指向原型对象,也就是构造函数.prototype == 原型对象 == 对象._proto_,那么对象就可以获取到原型对象中的属性和方法啦。

同时,所有对象中都有一个constructor属性,原型对象的constructor指向其对应的构造函数。

使用原型,就意味着我们可以把希望实例共享的属性和方法放到原型对象中去,而不是放在构造函数中,这样每一次通过构造函数new一个实例,原型对象中定义的方法都不会重新创建一次。来看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person() {
}

Person.prototype.name = "leon";
Person.prototype.age = "20";
Person.prototype.greeting = function() {
alert('Hi!');
};

var person1 = new Person();
var person2 = new Person();
alert(person1.name); //"leon"
alert(person2.name); //"leon"
alert(person1.greeting == person2.greeting); //true

优缺点

  • 优点:与单纯使用构造函数不一样,原型对象中的方法不会在实例中重新创建一次,节约内存。

  • 缺点:使用空构造函数,实例 person1 和 person2 的 name都一样了,我们显然不希望所有实例属性方法都一样,它们还是要有自己独有的属性方法。并且如果原型中对象中有引用类型值,实例中获得的都是该值的引用,意味着一个实例修改了这个值,其他实例中的值都会相应改变。

解决办法:构造函数+原型模式组合使用。

另外 JS 中还定义了一些与原型相关的属性,这里罗列一下:

取得实例的原型对象

1
Object.getPrototypeOf(person1)

判断是不是一个实例的原型对象

1
Person.prototype.isPrototypeOf(person1)

检测一个属性是否存在于实例中

1
person1.hasOwnProperty("name")

判断一个属性是否存在于实例和原型中

1
"name" in person1

构造函数+原型模式

定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name, age) {
this.name = name;
this.age = age;
}

Person.prototype = {
constructor: Person,
nationality: "China",
showinfo: function () {
console.info(this.name);
}
}

export default Person;

调用

1
2
3
4
5
<script type="module">
import Person from "./person.js";
let person = new Person("小明", "18");
person.showinfo();
</script>

上面代码中用对象字面量的形式重写了原型对象,这样相当于创建了一个新的对象,那么它的constructor属性就会指向Object,这里为了让它继续指向构造函数,显式的写上了constructor: Person

这种构造函数与原型模式混成的模式,是目前在 JS 中使用最为广泛的一种创建对象的方法。

class(ES6)

class 相对 function 是后出来的,既然 class 出来了,显然是为了解决 function 在处理面向对象设计中的缺陷而来。

class 作为 ES6 中的重大升级之一,有哪些优点呢?

  1. class 写法更加简洁、含义更加明确、代码结构更加清晰。
  2. class 尽管也是函数,却无法直接调用(不存在防御性代码了)。
  3. class 不存在变量提升。
  4. class 未污染 window 等全局变量。
  5. class 函数体中的代码始终以严格模式执行(新手写的代码可靠性也必须上来了)
  6. 可直接使用 set 和 get 函数。这比 function 要好用多了。
  7. class 内部方法中若涉及到 this,则一定要注意。class 中的 this 永远都不会指向 window。
  8. class 可以从 javascript 中著名的几大类中进行继承:Array、number、string….,显然 function 是做不到的。
  9. class 中有一个对象super,这个对象可以取到父类的方法、构造函数等。
  10. class 中不存在实例方法,class 中定义所有方法都是原型方法。这些方法也都是不可枚举的,使用 for in 这种方式无法遍历到它们。

定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
//构造方法
constructor(name, age) {
this.name = name;
this.age = age;
}

//方法
showinfo(){
console.info(this.name + ":" + this.#age);
};
}

export default Person;

注意

实例的属性必须定义在类的方法里。

调用

1
2
3
4
5
<script type="module">
import Person from "./person.js";
let person = new Person("小明", "18");
person.showinfo();
</script>

静态方法

1
2
3
4
5
6
7
class Foo {
static classMethod() {
return 'hello';
}
}

Foo.classMethod() // 'hello'

用实例调用静态方法会报错

1
2
3
var foo = new Foo();
foo.classMethod();
// TypeError: foo.classMethod is not a function