0
点赞
收藏
分享

微信扫一扫

你对JavaScript面向对象了解多少?

你对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,注意按照约定构造函数的首字母要大写。
它和工厂模式有什么区别?

  1. 没有显示的创建对象
  2. 直接将属性和方法赋值给了this对象
  3. 没有return语句
  4. 创建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!!!

  1. 我为要要在全局作用域中定义一个,只能被某个对象调用的函数呢?
  2. 如果,这个对象有多个方法,那我得在全局作用域中定于多个函数。。。这让我们如何去优(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!
来谈谈这种方式有哪些问题:

  1. 不能给构造函数传递初始化参数,因此,所有实例在默认情况下都将取得相同的属性值。
  2. 共享问题
    假如原型的属性中包含引用类型,在实例中修改该属性的值,那么,其他实例中对应的属性的值,也会被修改。

因此开发者很少单独使用这种方式来创建对象。

组合使用构造函数模式和原型模式

在实际开发过程中,我们使用构造函数模式来定义实例属性,而原型模式用于定义方法和共享的属性:

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 操作符,经历了哪些步骤

  1. 创建一个新的对象;
  2. 将构造函数的作用域赋给新的对象(因此,this就指向了新的对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新的对象。

构造函数和普通函数的区别

构造函数和其他函数的唯一区别,就在于调用他们的方式不同。
任何函数,只要是 通过 new 操作符来调用,那它就可以作为构造函数;
任何函数,如果不通过 new 操作符来调用,那它和普通的函数没什么两样。

原型对象的问题

  1. 不能给构造函数传递初始化参数,因此,所有实例在默认情况下都将取得相同的属性值。
  2. 共享问题
    假如原型的属性中包含引用类型,在实例中修改该属性的值,那么,其他实例中对应的属性的值,也会被修改。

结束

终终于整理完毕,感觉每次更新都像是难产。不过感觉自己又回到了两年前,初识javaScript,拿着红宝书迷茫的啃。现在依旧迷茫,只是在迷茫的路上,坚定了一点。

本文也收录在个人博客上lostimever.github.io

参考

  • 《JavaScript高级程序设计》第3版
举报

相关推荐

0 条评论