【JS】探索JavaScript对象构造函数都有哪些模式

探索JavaScript对象构造函数都有哪些模式

秦司令发布于 2 月 2 日

为什么要出模式这概念,每个模式的出现都是解决一种问题,当然每个模式都是有利有弊的。

模式它能干什么,它能帮助我们代码简洁,且更容易维护,代码不冗余。

这里借用修言大佬的理解:

工厂模式

工厂模式在软件工程领域是一种广为人知的设计模式,这种模式抽象了创建对象的具体过程。

该模式防止一个接口创建出很多对象,从而产生大量重复代码。

function Person(name, age) {

let o = new Object();

o.name = name;

o.age = age

return o;

}

let person1 = Person("蛙人", 23)

let person2 = Person("蛙人一号", 23)

上面example中,可以无限的调用该函数,它每次都会返回2个属性,工厂模式虽然解决了创建多个相似对象的问题。但没有解决对象识别的问题,因为所有的实例都指向一个原型,然而又一个新模式诞生了。

寄生构造函数模式

这种模式的基本思想就是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后在返回新创建的对象。

function Person(name, age) {

let o = new Object();

o.name = name;

o.age = age

return o

}

上面example中,Person函数创建了一个新函数,新增了属性,最后又返回这个对象,这跟工厂函数是一模一样的, 一般不常用。

构造函数模式

js中构造函数可以用来创建特定类型的对象,像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。

此外,也可以创建自定义构造函数,从而定义自定义对象类型和方法,使用构造函数将上面的example重写。

function Person(name, age) {

this.name = name;

this.age = age

}

let person = new Person('蛙人', 23)

上面example代码中和上面工厂模式代码不同之处在于

  1. 没有显示的创建对象
  2. 直接将属性和方法赋值给了this
  3. 没有return语句

此外,构造函数必须写new操作符,那么new的过程发生了什么

  1. 在函数内部隐式的创建一个对象
  2. 将构造函数的作用域赋值个新对象(因此this就指向了这个对象)
  3. 执行函数中的代码,this.xxx 这时这个this就是该函数对象,就可以添加属性和方法啦
  4. 隐式的返回return this

在上面person实例上有一个constructor属性,该属性指向Person函数实例

console.log(person1.constructor == Person)

constructor属性最初是作为对象的类型,我们可以还有instanceof来查看。

console.log(person instanceof Object) // true

console.log(person instanceof Person) // true

instanceof是查看前者的是prototype属性是不是指向后者的prototype


1. 将构造函数当做函数

构造函数和普通函数的唯一区别,它们之间就是调用方式不同。不过构造函数也是函数,不存在特殊语法,任何函数只要通过new操作符来调用,那它就可以作为构造函数,反之,任何函数不通过new操作符来调用,那它跟普通函数也是一样的,上面的Person函数可以通过下列方式来调用。

// 通过new操作符来调用

let person = new Person('蛙人', 23)

console.log(person.name) // 蛙人

// 普通方式调用

Person('蛙人', 23)

console.log(window.name) // 蛙人

// 在另一个作用域中调用

let o = new Object()

Person.call(o, '蛙人', 23)

o.name

2. 构造函数的弊端

构造函数虽然好用,代码也不冗余。但是如果我们要在构造函数中定义方法,则需要在每个实例里面都创建一个,我们知道在js中函数也是对象,创建一个函数也是相当于初始化一个对象。看下列代码

function Person(name, age) {

this.name = name;

this.age = age;

this.sayName = function() {

return this.name

}

}

function Animal() {

this.name = name;

this.weight = weight

this.sayName = function() {

return this.name

}

}

let person = new Person('蛙人', 23)

let animal = new Animal('老虎', 120)

console.log(person.sayName == animal.sayName) // false

上面example中定义了两个实例,每个实例都有sayName方法,不同实例上,同名函数名是不相等的。我们觉得创建两个不同的同样的功能确实没必要,我们改写一下,把sayName函数挂载到全局作用域中。

function Person(name, age) {

this.name = name;

this.age = age;

this.sayName = sayName

}

function Animal() {

this.name = name;

this.weight = weight

this.sayName = sayName

}

function sayName() {

return this.name

}

上面example中,我们把sayName函数定义在全局作用域中,两个实例现在就可以共享该方法,但是如果我们有多个共享函数,这样定义在全局作用域里面,就没有什么封装可言了。构造函数的弊端出现,又诞生了一个新的模式, 原型模式

原型模式

function Person() {}

Person.prototype.name = "蛙人"

Person.prototype.age = 23

Person.prototype.sayName = function() {

return this.name;

}

let person1 = new Person()

console.log(person1.sayName()) // 蛙人

let person2 = new Person()

console.log(person1.sayName == person2.sayName) // true

上面example中,我们访问的person1实例的属性和方法都是访问的原型对象上的, 如果构造函数中没有,就会去找该对象的原型上。

1.理解原型对象

无论什么时候,只要创建一个函数,该函数就会存在一个prototype属性,这个属性指向函数的原型对象,所有原型对象都会自动获得一个constructor构造函数属性,这个属性包含一个指向prototype属性所在函数的指针,也就是说这个prototype属性的主人是谁。

创建一个空的自定义构造函数之后,其原型对象默认只会取得constructor属性,至于其它的属性和方法都是从Object继承而来,当调用构造函数之后,该函数内部包含一个指针(__proto__),指向构造函数的原型对象,js官方术语管这个叫做[[Prototype]],虽然没有明确标准方式访问[[Prototype]],但Firefox、Safari、Chrome在每个对象上都支持一个__proto__属性,不过最重要的一点是,这个属性__proto__指向构造函数的原型对象,而不是构造函数。

<div align=center>

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b03bcdb1f1784537a936145739ef9599~tplv-k3u1fbpfcp-zoom-1.image) <br>

<span>图片来自js红宝书</span>

</div>

可以看到上图中,Person的原型和属性以及和两个person实例的关系。Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person构造函数,那么我们怎么查看一个实例上有没有别的原型对象呢,Es5版本中为我们提供了Api,isPrototypeOf

function Person() {

this.name = "蛙人"

this.age = 23

}

let o = {}

o.__proto__ = Person.prototype

Person.prototype.isPrototypeOf(o) // true 判断o的原型是不是指向Person构造函数原型对象

Es5中还新增了一个方法,Object.getPrototypeOf获取当前的原型对象。

function Person() {

this.name = "蛙人"

this.age = 23

}

let o = {}

o.__proto__ = Person.prototype

Object.getPrototypeOf(o) // 获取当前原型对象

回过头来,继续说原型模式。虽然可以通过对象实例访问保存在原型中值,但我们不能改写原型中的值,如果我们在实例中添加了一个属性,而该属性和原型中的属性名一样的话,那么就会在实例中创建该属性,该属性就会屏蔽原型中的那个属性,大白话就是,我们再次访问这个属性时,访问的就是实例中的属性,而不是原型上的属性。

function Person() {}

Person.prototype.name = "张三"

let person = new Person();

person.name = "蛙人"

console.log(person.name) // 蛙人

上面example中,给person实例上添加了一个属性再次访问是,先会去找实例上的属性,如果实例上有这个属性则返回属性值,如果没有则去原型上找。

2. 如何知道某个属性是原型上的还是实例上的

那么怎么知道这个属性是原型上的属性,还是实例上的属性呢,Es5中提供了查看这个属性是不是在这个对象的实例上,Object.hasOwnProperty()

function Person() {}

Person.prototype.name = "张三"

let person = new Person();

console.log(person.hasOwnProperty('name')) // false

person.name = "蛙人"

console.log(person.hasOwnProperty('name')) // true

还有一个方法是从原型上查找属性,in操作符。

function Person() {}

Person.prototype.name = "张三"

let person = new Person();

console.log("name" in person) // true

person.name = "蛙人"

console.log("name" in person) // true

上面example中,in操作符是先从实例上去查找属性,如果找不到再去原型上找。

3.更简洁的原型对象

在前面的例子中每次我们都需要写一遍Person.prototype,可以用一个对象字面量的形式来重写整个对象,请看下列

function Person() {}

Person.prototype = {

age: 23,

name: "蛙人",

sayName() {

}

}

let person = new Person()

console.log(person instanceof Person) // true

console.log(person instanceof Object) // true

console.log(person.constructor == Person) // false

console.log(person.constructor == Object) // true

上面example中,我们都知道创建一个函数就会默认自带一个prototype函数,但是上面代码中,我们把prototype定义为字面量形式,相当于是重写了该对象,重写完prototype之后我们这里的constructor就指向Object函数

4.原型对象的问题

原型模式也不是没有缺点。为了省略初始化参数,结果所有的实例都在原型上定义,默认情况下都取得相同的属性值。虽然这会在某种程度上带来不便,但还不是原型最大问题。

原型中所有属性是被很多实例共享的,这种共享如果对于函数方法来说非常合适,但是如果出现引用类型的话,问题就比较棘手了

function Person() {}

Person.prototype = {

name: "蛙人",

age: 23,

hobby: ["打球", "撩妹"]

}

let person = new Person()

let person1 = new Person()

person.hobby.push("玩游戏")

console.log(person.hobby) // ["打球", "撩妹", "玩游戏"]

console.log(person1.hobby) // ["打球", "撩妹", "玩游戏"]

上面example中,就看到了问题,给person实例上的hobby属性添加了一个值,然而person1的实例上的hobby属性值也变了。所以这就是原型模式的问题。

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

我们使用这种模式在把上面代码写一下,我们在工作中,创建自定义最常见的构造函数方法就是,使用组合构造函数和原型模式,构造函数模式用来只定义属性,原型上只定义方法,这样每个实例都会拥有实例属性的副本,但又共享着方法,最大限度节省了内存。

function Person(name, age) {

this.name = name;

this.age = age;

this.hobby = ["打球", "撩妹"]

}

let person = new Person("蛙人", 23)

let person1 = new Person("小蛙", 20)

person.hobby.push("玩游戏")

console.log(person1.hobby) // ["打球", "撩妹"]

上面example中,在构造函数实例里定义属性,这样每次重新调用一个实例时,构造函数也会重新初始化一下,从而每个实例互不影响。

动态原型模式

在其它OO(面向对象)语言中开发人员经常看到独立的构造函数和原型时,可能会很懵逼。动态原型的写法就是把原型和实例属性封装在构造函数内,而通过构造函数初始化原型

function Person(name, age) {

this.name = name;

this.age = age;

if (typeof this.sayName != "function") {

Person.prototype.sayName = function() {

return this.name

}

}

}

let person = new Person("蛙人", 23)

person.sayName()

上面example中,调用函数初始化时,会直接往给原型上添加sayName方法。这里对原型的添加,会立即对实例中产生改变。

稳妥构造函数模式

所谓稳妥对象,指的就是没有公共属性,就是在一定的环境下,不使用thisnew,稳妥对象最适合在一些安全环境中(这些环境下是禁用thisnew),稳妥函数模式有点和寄生构造函数相似,但是有两点不同:一是创建函数不用this,二是不使用new操作符调用,现在我们将上面例子重新写一下。

function Person(name, age) {

let o = new Object()

o.name = name;

o.age = age;

return o

}

let person = Person("蛙人", 23)

觉得写的不错那就点个赞叭!

javascript

阅读 18发布于 2 月 2 日

本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议

avatar

秦司令

前端白菜一枚~

1 声望

1 粉丝

0 条评论

得票时间

avatar

秦司令

前端白菜一枚~

1 声望

1 粉丝

宣传栏

为什么要出模式这概念,每个模式的出现都是解决一种问题,当然每个模式都是有利有弊的。

模式它能干什么,它能帮助我们代码简洁,且更容易维护,代码不冗余。

这里借用修言大佬的理解:

工厂模式

工厂模式在软件工程领域是一种广为人知的设计模式,这种模式抽象了创建对象的具体过程。

该模式防止一个接口创建出很多对象,从而产生大量重复代码。

function Person(name, age) {

let o = new Object();

o.name = name;

o.age = age

return o;

}

let person1 = Person("蛙人", 23)

let person2 = Person("蛙人一号", 23)

上面example中,可以无限的调用该函数,它每次都会返回2个属性,工厂模式虽然解决了创建多个相似对象的问题。但没有解决对象识别的问题,因为所有的实例都指向一个原型,然而又一个新模式诞生了。

寄生构造函数模式

这种模式的基本思想就是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后在返回新创建的对象。

function Person(name, age) {

let o = new Object();

o.name = name;

o.age = age

return o

}

上面example中,Person函数创建了一个新函数,新增了属性,最后又返回这个对象,这跟工厂函数是一模一样的, 一般不常用。

构造函数模式

js中构造函数可以用来创建特定类型的对象,像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。

此外,也可以创建自定义构造函数,从而定义自定义对象类型和方法,使用构造函数将上面的example重写。

function Person(name, age) {

this.name = name;

this.age = age

}

let person = new Person('蛙人', 23)

上面example代码中和上面工厂模式代码不同之处在于

  1. 没有显示的创建对象
  2. 直接将属性和方法赋值给了this
  3. 没有return语句

此外,构造函数必须写new操作符,那么new的过程发生了什么

  1. 在函数内部隐式的创建一个对象
  2. 将构造函数的作用域赋值个新对象(因此this就指向了这个对象)
  3. 执行函数中的代码,this.xxx 这时这个this就是该函数对象,就可以添加属性和方法啦
  4. 隐式的返回return this

在上面person实例上有一个constructor属性,该属性指向Person函数实例

console.log(person1.constructor == Person)

constructor属性最初是作为对象的类型,我们可以还有instanceof来查看。

console.log(person instanceof Object) // true

console.log(person instanceof Person) // true

instanceof是查看前者的是prototype属性是不是指向后者的prototype


1. 将构造函数当做函数

构造函数和普通函数的唯一区别,它们之间就是调用方式不同。不过构造函数也是函数,不存在特殊语法,任何函数只要通过new操作符来调用,那它就可以作为构造函数,反之,任何函数不通过new操作符来调用,那它跟普通函数也是一样的,上面的Person函数可以通过下列方式来调用。

// 通过new操作符来调用

let person = new Person('蛙人', 23)

console.log(person.name) // 蛙人

// 普通方式调用

Person('蛙人', 23)

console.log(window.name) // 蛙人

// 在另一个作用域中调用

let o = new Object()

Person.call(o, '蛙人', 23)

o.name

2. 构造函数的弊端

构造函数虽然好用,代码也不冗余。但是如果我们要在构造函数中定义方法,则需要在每个实例里面都创建一个,我们知道在js中函数也是对象,创建一个函数也是相当于初始化一个对象。看下列代码

function Person(name, age) {

this.name = name;

this.age = age;

this.sayName = function() {

return this.name

}

}

function Animal() {

this.name = name;

this.weight = weight

this.sayName = function() {

return this.name

}

}

let person = new Person('蛙人', 23)

let animal = new Animal('老虎', 120)

console.log(person.sayName == animal.sayName) // false

上面example中定义了两个实例,每个实例都有sayName方法,不同实例上,同名函数名是不相等的。我们觉得创建两个不同的同样的功能确实没必要,我们改写一下,把sayName函数挂载到全局作用域中。

function Person(name, age) {

this.name = name;

this.age = age;

this.sayName = sayName

}

function Animal() {

this.name = name;

this.weight = weight

this.sayName = sayName

}

function sayName() {

return this.name

}

上面example中,我们把sayName函数定义在全局作用域中,两个实例现在就可以共享该方法,但是如果我们有多个共享函数,这样定义在全局作用域里面,就没有什么封装可言了。构造函数的弊端出现,又诞生了一个新的模式, 原型模式

原型模式

function Person() {}

Person.prototype.name = "蛙人"

Person.prototype.age = 23

Person.prototype.sayName = function() {

return this.name;

}

let person1 = new Person()

console.log(person1.sayName()) // 蛙人

let person2 = new Person()

console.log(person1.sayName == person2.sayName) // true

上面example中,我们访问的person1实例的属性和方法都是访问的原型对象上的, 如果构造函数中没有,就会去找该对象的原型上。

1.理解原型对象

无论什么时候,只要创建一个函数,该函数就会存在一个prototype属性,这个属性指向函数的原型对象,所有原型对象都会自动获得一个constructor构造函数属性,这个属性包含一个指向prototype属性所在函数的指针,也就是说这个prototype属性的主人是谁。

创建一个空的自定义构造函数之后,其原型对象默认只会取得constructor属性,至于其它的属性和方法都是从Object继承而来,当调用构造函数之后,该函数内部包含一个指针(__proto__),指向构造函数的原型对象,js官方术语管这个叫做[[Prototype]],虽然没有明确标准方式访问[[Prototype]],但Firefox、Safari、Chrome在每个对象上都支持一个__proto__属性,不过最重要的一点是,这个属性__proto__指向构造函数的原型对象,而不是构造函数。

<div align=center>

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b03bcdb1f1784537a936145739ef9599~tplv-k3u1fbpfcp-zoom-1.image) <br>

<span>图片来自js红宝书</span>

</div>

可以看到上图中,Person的原型和属性以及和两个person实例的关系。Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person构造函数,那么我们怎么查看一个实例上有没有别的原型对象呢,Es5版本中为我们提供了Api,isPrototypeOf

function Person() {

this.name = "蛙人"

this.age = 23

}

let o = {}

o.__proto__ = Person.prototype

Person.prototype.isPrototypeOf(o) // true 判断o的原型是不是指向Person构造函数原型对象

Es5中还新增了一个方法,Object.getPrototypeOf获取当前的原型对象。

function Person() {

this.name = "蛙人"

this.age = 23

}

let o = {}

o.__proto__ = Person.prototype

Object.getPrototypeOf(o) // 获取当前原型对象

回过头来,继续说原型模式。虽然可以通过对象实例访问保存在原型中值,但我们不能改写原型中的值,如果我们在实例中添加了一个属性,而该属性和原型中的属性名一样的话,那么就会在实例中创建该属性,该属性就会屏蔽原型中的那个属性,大白话就是,我们再次访问这个属性时,访问的就是实例中的属性,而不是原型上的属性。

function Person() {}

Person.prototype.name = "张三"

let person = new Person();

person.name = "蛙人"

console.log(person.name) // 蛙人

上面example中,给person实例上添加了一个属性再次访问是,先会去找实例上的属性,如果实例上有这个属性则返回属性值,如果没有则去原型上找。

2. 如何知道某个属性是原型上的还是实例上的

那么怎么知道这个属性是原型上的属性,还是实例上的属性呢,Es5中提供了查看这个属性是不是在这个对象的实例上,Object.hasOwnProperty()

function Person() {}

Person.prototype.name = "张三"

let person = new Person();

console.log(person.hasOwnProperty('name')) // false

person.name = "蛙人"

console.log(person.hasOwnProperty('name')) // true

还有一个方法是从原型上查找属性,in操作符。

function Person() {}

Person.prototype.name = "张三"

let person = new Person();

console.log("name" in person) // true

person.name = "蛙人"

console.log("name" in person) // true

上面example中,in操作符是先从实例上去查找属性,如果找不到再去原型上找。

3.更简洁的原型对象

在前面的例子中每次我们都需要写一遍Person.prototype,可以用一个对象字面量的形式来重写整个对象,请看下列

function Person() {}

Person.prototype = {

age: 23,

name: "蛙人",

sayName() {

}

}

let person = new Person()

console.log(person instanceof Person) // true

console.log(person instanceof Object) // true

console.log(person.constructor == Person) // false

console.log(person.constructor == Object) // true

上面example中,我们都知道创建一个函数就会默认自带一个prototype函数,但是上面代码中,我们把prototype定义为字面量形式,相当于是重写了该对象,重写完prototype之后我们这里的constructor就指向Object函数

4.原型对象的问题

原型模式也不是没有缺点。为了省略初始化参数,结果所有的实例都在原型上定义,默认情况下都取得相同的属性值。虽然这会在某种程度上带来不便,但还不是原型最大问题。

原型中所有属性是被很多实例共享的,这种共享如果对于函数方法来说非常合适,但是如果出现引用类型的话,问题就比较棘手了

function Person() {}

Person.prototype = {

name: "蛙人",

age: 23,

hobby: ["打球", "撩妹"]

}

let person = new Person()

let person1 = new Person()

person.hobby.push("玩游戏")

console.log(person.hobby) // ["打球", "撩妹", "玩游戏"]

console.log(person1.hobby) // ["打球", "撩妹", "玩游戏"]

上面example中,就看到了问题,给person实例上的hobby属性添加了一个值,然而person1的实例上的hobby属性值也变了。所以这就是原型模式的问题。

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

我们使用这种模式在把上面代码写一下,我们在工作中,创建自定义最常见的构造函数方法就是,使用组合构造函数和原型模式,构造函数模式用来只定义属性,原型上只定义方法,这样每个实例都会拥有实例属性的副本,但又共享着方法,最大限度节省了内存。

function Person(name, age) {

this.name = name;

this.age = age;

this.hobby = ["打球", "撩妹"]

}

let person = new Person("蛙人", 23)

let person1 = new Person("小蛙", 20)

person.hobby.push("玩游戏")

console.log(person1.hobby) // ["打球", "撩妹"]

上面example中,在构造函数实例里定义属性,这样每次重新调用一个实例时,构造函数也会重新初始化一下,从而每个实例互不影响。

动态原型模式

在其它OO(面向对象)语言中开发人员经常看到独立的构造函数和原型时,可能会很懵逼。动态原型的写法就是把原型和实例属性封装在构造函数内,而通过构造函数初始化原型

function Person(name, age) {

this.name = name;

this.age = age;

if (typeof this.sayName != "function") {

Person.prototype.sayName = function() {

return this.name

}

}

}

let person = new Person("蛙人", 23)

person.sayName()

上面example中,调用函数初始化时,会直接往给原型上添加sayName方法。这里对原型的添加,会立即对实例中产生改变。

稳妥构造函数模式

所谓稳妥对象,指的就是没有公共属性,就是在一定的环境下,不使用thisnew,稳妥对象最适合在一些安全环境中(这些环境下是禁用thisnew),稳妥函数模式有点和寄生构造函数相似,但是有两点不同:一是创建函数不用this,二是不使用new操作符调用,现在我们将上面例子重新写一下。

function Person(name, age) {

let o = new Object()

o.name = name;

o.age = age;

return o

}

let person = Person("蛙人", 23)

觉得写的不错那就点个赞叭!

以上是 【JS】探索JavaScript对象构造函数都有哪些模式 的全部内容, 来源链接: www.h5w3.com/112716.html

度小满广告!风险提示:广告信息均来自平台方,不代表平台安全性,不构成建议!
度小满
回到顶部