你对JavaScript面向对象了解多少?
前言
前两天看到一个有意思的观点:工具的进步,不代表你能力的进步。前端框架风起云涌,我们用得得心应手,回过头来,脱离框架我们还剩下什么?我觉得这是个值得深思的问题。
扯远了,本文主要是想把JavaScript
中面向对象的知识做一个整理和回顾,加深印象。
怎怎怎么找对象?
new 一个对象
没有对象怎么办?
new 一个!
let obj = new Object();
obj.name = 'object';
obj.value = 11;
obj.methods = function() {
console.log('this is a object')
};
这便是创建一个对象最简单的方式。但是,每次都要 new
一个,复杂又麻烦,有没有更简单的方式呢?
往下看
使用字面量创建
what?啥是字面量?
字面量:literals
,有些书上叫做直接量。看见什么,它就是什么
举个栗子:
let obj = {
name : 'object';
value : 11;
methods : function() {
console.log('this is a object')
};
};
简单粗暴?!事实上,如果是简单的创建几个对象,使用字面量创建对象无可厚非,但若有很多相似对象需要创建,这种方式便会产生大量的重复代码,显然这是很不友好的。
于是工厂模式应运而生。
工厂模式
function createFactory(name, value) {
let obj = new Object();
obj.name = name;
obj.value = value;
obj.methods = function() {
console.log('this is a object, my name is ' + this.name)
};
return obj;
}
let createFactory1 = factory('saints', 12)
let createFactory2 = factory('Google', 50)
工厂模式虽然解决了创建 多个相似对象的问题,但却没有解决对象识别的问题,也就是说,无法区分它们的对象类型。
这该怎么办呢?
构造函数模式
先用构造函数模式重写上面的例子:
function Factory(name, value) {
this.name = name;
this.value = value;
this.methods = function() {
console.log('this is a object, my name is ' + this.name)
};
}
let factory1 = new Factory('saints', 12);
let factory2 = new Factory('Google', 50);
这里我们使用一个大写字母F开头的构造函数替代了上例中的createFactory
,注意按照约定构造函数的首字母要大写。
它和工厂模式有什么区别?
- 没有显示的创建对象
- 直接将属性和方法赋值给了
this
对象 - 没有
return
语句 - 创建
Factory
实例时,必须使用new
操作符
构造函数大法好啊,只不过它也不是万能的,最大的问题是,它的每个方法都要在每个实例上重新创建一次。
换句话说,两个实例中调用的构造函数中的method
方法不是同一个Function
实例:
console.log(factory1.method === factory2.method) // false
为啥会这样呢?
不要忘了,ECMAScript
中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。
我们可以把
this.methods = function() {
console.log('this is a object, my name is ' + this.name)
};
看成:
this.methods = new Function() {
console.log('this is a object, my name is ' + this.name)
};
这样看是不是更加清楚了呢?
调用同一个方法,却声明了不同的实例,实在浪费资源。大可像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。
function Factory(name, value) {
this.name = name;
this.value = value;
this.methods = methods
}
function methods() {
console.log('this is a object, my name is ' + this.name)
}
let factory1 = new Factory('saints', 12);
let factory2 = new Factory('Google', 50);
堪称完美。
But!!!
- 我为要要在全局作用域中定义一个,只能被某个对象调用的函数呢?
- 如果,这个对象有多个方法,那我得在全局作用域中定于多个函数。。。这让我们如何去优(zhuang)雅(bi)的封装一个对象呢?
好在, 这些问题可以通过使用原型模式来解决。
原型模式
我们每创建一个函数,都有一个prototype
(原型)属性,这个属性是一个指针,指向一个对象。也就是说,prototype
就是,通过调用构造函数创建的那个对象实例的原型对象。看到这我已经晕了。
使用原型对象的好处是:可以让所有对象实例共享它所包含的属性和方法。
上代码!
function Factory() { }
Factory.prototype.name = 'saints';
Factory.prototype.value = 12;
Factory.prototype.methods = function() {
console.log('this is a object, my name is ' + this.name)
}
let factory1 = new Factory();
factory1.methods(); // this is a object, my name is saints
let factory2 = new Factory();
factory2.methods(); // this is a object, my name is saints
console.log(factory1.methods === factory2.methods) // true
这样就完美的解决了属性和方法共享的问题,所有的实例共享同一组属性和方法。
我们要知其然,还要知其所以然,原型模式的原理是什么呢?
通过下面的原型链,一目了然:
在默认情况下,所有原型对象都会自动获得一个 constructor
(构造函数)属性,这个属性包含一个指向 prototype
属性所在函数的指针,图中,Factory.prototype
指向了原型对象,而 Factory.prototype.constructor
又指回了 Factory
每当代码读取某个对象的某个属性时,都会执行一次搜索,首先会询问实例对象中有没有该属性,如果没有则继续查找原型对象(这就是执行期上下文)
let factory1 = new Factory();
factory1.name = 'google'
let factory2 = new Factory();
console.log(factory1.name); // google
console.log(factory2.name); // saints
当为对象实例添加一个属性时, 这个属性屏蔽原型对象中的同名属性,注意是屏蔽,这只会阻止我们去访问这个同名属性,而不会对它做修改。即使将该属性修改为null
,也不会恢复我们对原型对象中同名属性的访问,除非使用delete
彻底删除该属性。
大家可以看到,每次新增一个属性,都要输入一次Factory.prototype
,为了减少不必要的输入,同时更加直观的封装原型对象的功能,我们使用字面量来重写整个原型对象:
function Factory() {}
Factory.prototype = {
name : 'saints';
value : 12;
methods : function() {
console.log('this is a object, my name is ' + this.name)
}
}
有个地方需要注意的是,以对象字面量形式创建的新对象,本质上完全重写了默认的 prototype
对象,因此,此时的Factory.prototype.constructor
已不再指向Factory
,而是指向了Object
。
let factory1 = new Factory();
console.log(factory1.constructor == Factory); //false
console.log(factory1.constructor == Object); //true
一般情况下,这种改变不会对我们造成困扰,如果 constructor
的值真的很重要,可以像下面这样特意将它设置回适当的值:
function Factory() {}
Factory.prototype = {
constructor: Factory,
name : 'saints';
value : 12;
methods : function() {
console.log('this is a object, my name is ' + this.name)
}
}
let factory1 = new Factory();
console.log(factory1.constructor == Factory); //true
你以为这样就完了么?too young too simple!
来谈谈这种方式有哪些问题:
- 不能给构造函数传递初始化参数,因此,所有实例在默认情况下都将取得相同的属性值。
- 共享问题
假如原型的属性中包含引用类型,在实例中修改该属性的值,那么,其他实例中对应的属性的值,也会被修改。
因此开发者很少单独使用这种方式来创建对象。
组合使用构造函数模式和原型模式
在实际开发过程中,我们使用构造函数模式来定义实例属性,而原型模式用于定义方法和共享的属性:
function Factory(name, value) {
this.name = name;
this.value = value;
}
Factory.prototype = {
constructor: Factory,
methods : function() {
console.log('this is a object, my name is ' + this.name)
}
}
let factory1 = new Factory('saints', 22);
console.log(factory1.constructor == Factory); //true
每个实例都会有自己的一份实例属性,但同时又共享着方法,最大限度的节省了内存,还支持传递初始参数,优点甚多。在ECMAScript
中是使用最广泛、认同度最高的一种创建自定义对象的方法。
动态原型模式
动态原型模式,把所有信息都封装在了构造函数中,在构造函数中初始化原型(仅在必要的情况下),可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Factory(name, value) {
this.name = name;
this.value = value;
if (typeof this.methods == 'function') {
Factory.prototype.methods = function() {
console.log('this is a object, my name is ' + this.name)
}
}
}
let factory1 = new Factory('saints', 22);
Factory
是一个构造函数,通过new Factory(...)
来生成实例对象。每当一个Factory的对象生成时,Factory内部的代码都会被调用一次。
如果去掉if的话,每new
一次(即每当一个实例对象生产时),都会重新定义一个新的函数,然后挂到Factory.prototype.methods
属性上。而实际上,你只需要定义一次就够了,因为所有实例都会共享此属性的。所以如果去掉if
的话,会造成没必要的时间和空间浪费;而加上if后,只在new
第一个实例时才会定义methods
方法,之后就不会了。
假设除了methods
方法外,你还定义了很多其他方法,比如sayBye、cry、smile
等等。此时你只需要把它们都放到对methods
判断的`if块里面就可以了。
if (typeof this.methods != "function") {
Factory.prototype.methods = function() {...};
Factory.prototype.sayBye = function() {...};
Factory.prototype.cry = function() {...};
...
}
万恶的面试题
使用 new 操作符,经历了哪些步骤
- 创建一个新的对象;
- 将构造函数的作用域赋给新的对象(因此,
this
就指向了新的对象); - 执行构造函数中的代码(为这个新对象添加属性);
- 返回新的对象。
构造函数和普通函数的区别
构造函数和其他函数的唯一区别,就在于调用他们的方式不同。
任何函数,只要是 通过 new
操作符来调用,那它就可以作为构造函数;
任何函数,如果不通过 new
操作符来调用,那它和普通的函数没什么两样。
原型对象的问题
- 不能给构造函数传递初始化参数,因此,所有实例在默认情况下都将取得相同的属性值。
- 共享问题
假如原型的属性中包含引用类型,在实例中修改该属性的值,那么,其他实例中对应的属性的值,也会被修改。
结束
终终于整理完毕,感觉每次更新都像是难产。不过感觉自己又回到了两年前,初识javaScript,拿着红宝书迷茫的啃。现在依旧迷茫,只是在迷茫的路上,坚定了一点。
本文也收录在个人博客上lostimever.github.io。
参考
- 《JavaScript高级程序设计》第3版