JavaScript面向对象的程序设计

工厂模式

由于 ES6 之前没有 class 概念,所以使用函数来封装的,工程模式采用最直接的传入参数创建对象并赋值,然后返回对象的方式

function Great(name,age) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.getName = function() {
    return this.name;
  }
  return o;
}

var g1 = Great('link',21);
console.log(g1.getName(),g1.age);//link 21

但是工厂模式无法知道一个对象的类型

构造函数模式

使用 new 操作符,并且函数内部无需创建对象,也不需要 return 语句

使用 new 操作符会实行以下操作

  • 当创建一个新对象时,将构造函数内部的 this 指向对象
  • 执行代码将值赋值给该对象
  • 返回这个对象

可以使用每个对象都拥有的属性 constructor 来检测其类型,也可以用 instanceof

function Great(name,age) {
  this.name = name;
  this.age = age;
  this.getName = function() {
    return this.name;
  }
}
var g1 = new Great('Bob',31);
var g2 = new Great('Tom',27);
console.log(g1.getName(),g1.age);//Bob 31
console.log(g2.getName(),g2.age);//Tom 27
console.log(g1.constructor == Great);//true
console.log(g2 instanceof Great);//true

当函数用

本身构造函数也是函数,对于任何函数,使用 new 操作符来调用,就可以当做构造函数;按照普通函数那样调用也就更平常函数差不多

问题

主要出现在对象的内部函数上,我们知道对象一般以堆内存的方法存储,也就是一堆变量和函数放在一堆,许多的对象就是许多这样的堆

对于构造函数里面定义的函数,创建的每个实例都拥有它,并且名字相同,看似这些函数都是同一个,其实不然,用上面一条来解释就知道,虽然他们拥有相同名字的函数,但是每个函数都在不同的堆里,互不影响。

问题就出在没必要每个实例都有这样的函数,毕竟调用函数时,都有每一个对象的 this 传入,依靠这个 this 就能确保调用函数时依据的是当前对象所拥有的值,不会冲突,所以他们其实可以共用一个,反正靠 this 指向就行。然而构造函数的方法就导致每个实例都有独自的函数,从而造成了大量内存浪费

此时可以使用一种方式解决

function Great(name,age) {
  this.name = name;
  this.age = age;
  this.getName = getName;
}
function getName() {
  return this.name;
}
var g1 = new Great('Bob',31);
var g2 = new Great('Tom',27);
console.log(g1.getName());//Bob
console.log(g2.getName());//Tom

通过定义全局函数的方式,让他们共用一个

然而这样又出现一个问题,当函数较多时,全部放在外面,不就与普通函数搞混了吗,而且不好维护。所以,就此出现另一种模式,原型模式

原型模式

问题引申:上面的构造函数说道,想要通过共同享用的方式来减少不必要的开销,但是太多的全局函数不利于和外部普通函数辨别,并且这也违背了封装的思想,所以原型就是允许以特殊的方式定义所有实例都可以共享的属性和方法,并且实例在共享的基础上还可以有属于自己的方法与属性

每一个函数包括构造函数,都有一个 prototype 属性,它是一个指针指向一个对象,该对象包含着特定类型的所有共享属性和方法

无论是构造函数还是普通函数,反正只要是函数,都会默认创建一个 prototype 用来指向一个对象,也就是原型对象,而这个对象默认就有一个值,也就是 constructor,用来指向这个构造函数(函数),所以这也是为什么一旦重写原型对象,就会导致 constructor 不在指向原来的构造函数,因为这个属性已经没有了

通常可以这么定义

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
  Great.prototype.getName = function() {
    return '原型->' + this.name;
  }
}
var g1 = new Great();
var g2 = new Great();
console.log(g1.age,g1.getName())//21 "原型->Greatiga"
console.log(g1.getName === g2.getName)// true

可以看到,上面的 getName 方法是共有的

理解原型对象

  • 原型对象储存着所有实例都可以共享的属性和方法
  • 所有的函数在创建时,都会根据一组特定规则创建一个 prototype 来指向函数数的原型对象
  • 每一个原型对象又会拥有一个 constructor 用来指向定义该原型对象的函数
  • 除此之外每个对象还拥有从 Object 继承而来的属性和方法
  • 指向原型的指针叫 [[Prototype]],但是没有标准的访问方式,可以靠 __proto__

总结

  • 每一个原型对象都有一个 constructor 属性指向构造它的构造函数
  • 每一个构造函数有一个 prototype 属性指向它的原型对象,可以动态修改这个属性
  • 每一个实例对象都有一个 [[Prototype]] 属性指向它所拥有的原型,通常不能直接访问修改,但可以通过 __proto__ 来查看

prototype.isPrototypeOf()

用来测试一个对象的原型是否是来自于某个类型

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
  Great.prototype.getName = function() {
    return '原型->' + this.name;
  }
}
function GG() {
  GG.prototype.name = 'no';
}
var g1 = new Great();
var g2 = new Great();
console.log(Great.prototype.isPrototypeOf(g1))//true
console.log(GG.prototype.isPrototypeOf(g2))//false

ES5 新增 Object.getPrototypeOf()

用来获取实例的原型对象,该方法直接返回原型对象

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
  Great.prototype.getName = function() {
    return '原型->' + this.name;
  }
}
var g1 = new Great();
console.log(Object.getPrototypeOf(g1))//{name: "Greatiga", age: 21, getName: ƒ, constructor: ƒ}

属性重写和 hasOwnProperty()

除了添加新属性以外,实例也可以重写同名的属性和方法 重写不会影响原型中本就有的同名属性和方法 解析器执行实例时,会从实例自身开始找是否有符合的属性或方法,有就执行,否则就到原型中找 可以删除重写的属性或方法,这样就可以访问原型中的属性和方法

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
  Great.prototype.getName = function() {
    return '原型->' + this.name;
  }
}
var g1 = new Great();
var g2 = new Great();
g1.getName = function() {
  return '实例->' + this.name;
}
g2.name = 'Tomk';
console.log(g1.getName())//实例->Greatiga
console.log(g2.getName())//原型->Tomk

delete g1.getName;
console.log(g1.getName())//原型->Greatiga

通过 hasOwnProperty() 方法测试一个属性来自于哪里,在实例中就为 true 否则为 false,这个方法是从 Object 那里继承而来的

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
  Great.prototype.getName = function() {
    return '原型->' + this.name;
  }
}
var g1 = new Great();
g1.getName = function() {
  return '实例->' + this.name;
}
console.log(g1.hasOwnProperty('name'));//false
console.log(g1.hasOwnProperty('getName'));//true

in 操作符与原型

通常用在两个地方

for-in 循环 属性 in 对象

属性 in 对象

可以与 hasOwnProperty 组合使用来判断对象来自于实例还是原型

  • in可以先判断一个属性是否存在
  • 通过 hasOwnProperty 又可以判断是否在实例中
function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
}
var g1 = new Great();
g1.name = 'Link';
console.log('name' in g1);//true
console.log('getName' in g1);//false

console.log((g1.hasOwnProperty('name')) && ('name' in g1));//true 
//在实例中
console.log((g1.hasOwnProperty('age')) && ('age' in g1))//false 
//在原型中

for-in

我们应用在对象上时遍历出来的是属性名字

  • 包括实例中所有属性
  • 包括原型中所有属性
  • 包括被标记为不可枚举的属性 Enumerable: false

上述条件的所有属性都会被遍历,IE8 之前的版本中,原型中被屏蔽不可枚举的属性无法被遍历,该问题在后续版本已修复

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
}
var g1 = new Great();
g1.area = 'China';
g1.toString = function() {
  return this.age;
}
for (post in g1) {
  console.log(post);
}
//area
//toString
//name
//age

Object.keys() 和 Object.getOwnPropertyNames()

可以获得对象上所有可以枚举的属性,该方法返回一个数组

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
}
var g1 = new Great();
g1.area = 'China';
g1.toString = function() {
  return this.age;
}

console.log(Object.keys(Great.prototype));//(2) ["name", "age"]
console.log(Object.keys(g1));//(2) ["area", "toString"]

可以看到,它会根据实例对象来确定遍历的属性,对象为原型就遍历原型,对象为实例对象就只遍历实例对象的属性

倘若要获取对象所有实例属性,则可以用 Object.getOwnPropertyNames()

function Great() {
  Great.prototype.name = 'Greatiga';
  Great.prototype.age = 21;
}
var g1 = new Great();
g1.area = 'China';
g1.toString = function() {
  return this.age;
}

console.log(Object.getOwnPropertyNames(Great.prototype));//(3) ["constructor", "name", "age"]
console.log(g1.constructor == Great);//true

简洁语法和动态性

原始定义原型的方式过于繁杂,可以简洁一点,但是这样的方式会导致 constructor 指向了 Object 而非构造函数,所以如果需要该值就得正确指定它的值

function Great() {}
Great.prototype = {
  name : 'Greatiga',
  age : 21,
  getName : function() {
    return this.age;
  }
};
var g1 = new Great();
console.log(g1.constructor == Great);//false
Great.prototype.constructor = Great;
console.log(g1.constructor == Great);//true

动态性就是指修改原型对象的属性后能即使在实例中反映出来,无论该实例的创建是在修改前还是修改后,都有响应;原因很简单,实例执行时,解析器都会从当前对象找起,一直到原型对象,当然也就能响应了

注意,修改原型对象时,不要用字面量的方式去修改,这会改变指针指向导致实例对象找不到原型。为什么呢?实例对象创建时会有一个属性(指针)指向原型对象,如果你只是修改原型中的对象那不会有事,因为你只是换了这堆砖头的一块砖,依然还是这堆砖,实例们依然可以找到

然而你直接用字面量方式去修改,就相当于弄了一堆新砖头,但是之前就已经有的实例对象还是指着原来那堆砖头并非你用字面量创建的新的一堆砖头,这不就导致该实例对象断开与原型的连接了吗,因为他现在存的那个指针值指向的不过是一堆没人要的砖头,说不定早就被当垃圾回收了呢

看下方例子

function Great() {}
Great.prototype = {
  name : 'Greatiga',
  age : 21,
};
var g = new Great();
Great.prototype.name = 'Great';
console.log(g.name);//Great
//此时正常,因为依然还是这个原型对象
Great.prototype = {
  name : 'Tom',
  age : 23,
};
console.log(g.name);//Great
//还是 Great?
g.__proto__ = Great.prototype;
console.log(g.name);//Tom

看上面倒数第三行那里,依然还是 Great 而不是 Tom,这并非程序错误,而是原型对象整个都变了,而实例 g 依然还是指着第一次改变名字后的那个原型对象,除非你改变实例 g 的指向,让它指向重写的那个原型对象,否则就永远找不到

原生对象的原型和原型存在的问题

原生对象

比如 Array,String,Date,RegExp这些原生对象,他们也一样拥有原型对象,我们不仅可以访问这些原型对象,还可以给它增加新属性

console.log(Array.prototype.map);//ƒ map() { [native code] }
console.log(String.prototype.toLocaleUpperCase);//ƒ toLocaleUpperCase() { [native code] }
console.log(Date.prototype.getYear);//ƒ getYear() { [native code] }
String.prototype.printf = function() {
  console.log(this.constructor)
};
console.log(String.prototype.printf);
//ƒ () {
//  console.log(this.constructor)
//}
console.log('Great'.printf());//ƒ String() { [native code] }

存在的问题

第一个,因为所有属性都由原型定义好了,所有一创建实例,实例对象就拥有所有属性,有的时候没必要这样

第二,比较关键,就比如某个原型属性被改变了,那么其他所有实例都会改变,如果我们想要每个实例都有自己的属性,即使这些这些属性同名,也要有不同的值,而仅靠原型对象时无法实现的,因为他们都共享同一个。

怎么解决?回想之前的构造函数模式不就可以吗,只是当时我们想要共享属性所以抛弃了,但是现在我们想要一部分共享,一部分特有,这样一来就引出了组合模式

构造函数和原型组合模式

组合这两种模式:构造函数模式定义实例可以独有的属性,原型模式定义所有实例共享的属性。这样一来,每个实例对象都有属于自己的实例属性,同时又可以共享部分共同的属性方法,从而最大限度节省了内存。

function Great(name,age) {
  this.name = name;
  this.age = age;
}
Great.prototype = {
  time : '2020-01-01',
  getName: function() {
    return this.name;
  },
  setAge: function(s) {
    this.age = s;
  },
  getAge : function() {
    return this.age;
  }
};
var g1 = new Great('Great',21);
var g2 = new Great('Never',20);
console.log(g1.getName(), g1.getAge());//Great 21
console.log(g1.time);//2020-01-01
g2.setAge(23);
console.log(g2.getName(), g2.getAge());//Never 23
console.log(g2.time);//2020-01-01

如上例子,两个对象拥有不同的名字和年龄,同时又共享一个时间

动态原型模式

引入此概念也是为了更好的封装,我们知道字面量方式定义原型对象时往往是在构造函数外部。动态原型模式可以将原型的定义与构造模式的赋值一起放入构造函数内部,采用这种方式在内部定义时,不要使用字面量的方式

function Great(name,age) {
  this.name = name;
  this.age = age;
  Great.prototype.time = '2020-01-01';
  Great.prototype.getName = function() {
    return this.name;
  },
  Great.prototype.setAge = function(s) {
    this.age = s;
  },
  Great.prototype.getAge = function() {
      return this.age;
  }
}
var g1 = new Great('Great',21);
var g2 = new Great('Never',20);
console.log(g1.getName(), g1.getAge());//Great 21
console.log(g1.time);//2020-01-01
g2.setAge(23);
console.log(g2.getName(), g2.getAge());//Never 23
console.log(g2.time);//2020-01-01

寄生原型模式

这一种模式旨在前面几种模式都无法使用的情况下使用,模式与普通的函数构造模式差不多,创建对象,赋值,返回对象,万不得已才用,而且红皮书上讲的也不是太细致,所以暂时不深究了

稳妥模式

在某些情境下,this 与 new 可能会不太安全,此时才用原有的创建对象、赋值、返回对象的方式来创建构造函数

  • 不使用 this 创建实例属性,不使用 new 调用构造函数

特别内容

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:

腾讯云开发者社区入驻申请

本文为原创文章,若文章内容出现抄袭雷同,请联系文章发布人或者网站管理员,我们将认真核实并及时删除。 除非另有说明,否则此博客中的所有文章均根据CC BY-NC-SA 4.0许可。如需转载请标明出处,谢谢配合!

END--感谢阅读

来发表你的感想吧~