初学JavaScript:JS中创建对象的模式(工厂模式/构造函数模式/原型模式/组合模式)

1、工厂模式

1.1工厂模式简介

虽然使用 Object 构造函数或对象字面量可以方便地创建
对象,但这些方式也有明显不足:创建具有同样接口的多个对象需要重复编写很多代码。

啥是工厂模式?

工厂模式是一种众所周知的设计模式,广泛应用于软件工程领域,用于抽象创建特定对象的过程。工厂模式是一种创建型模式,简单来说,工厂模式就是批量创建对象的一种方式。

工厂模式的作用

作用:批量创建同类型对象;降低代码冗余度
工厂模式,工厂,很明显,就是向工厂一样生产出人所想要的物品(对象)

为什么创建对象用工程模式?

首先,我们先看下普通方法模式创建对象

  • 字面量方式创建对象

字面量就是用来描述变量的;一般来说,给变量赋值的时候,等号右边的都可以看作是字面量(因为等号右边的都是用来描述这个变量的,比如描述一个变量为字符串(字符串字面量)、一个数组(数组字面量)、一个对象(对象字面量),等等)。
字面量方式创建对象的缺点是创造出来的这个对象是一次性的,如果需要用到四十次这个对象,那么就要创建四十次,比较麻烦。

  • new Object()创建对象

Object是JavaScript提供的构造函数;new Object()就是利用JavaScript提供的构造函数实例化了一个对象。
构造方法创建对象的缺点是这样创建写的代码看起来不像一个整体。它先实例化了一个对象,然后再为对象添加属性,这样就看不出来是个整体(像上面的用字面量来创建,属性都包在一个大括号里面,这样就很好看出这是个整体)。

1.2 工厂模式创建对象

//将创建对象的代码封装在一个函数中
function createPerson(name, age, gender) {
  var person = new Object();
  person.name = name;
  person.age = age;
  person.gender = gender;
  person.sayName = function () {
    console.log(this.name);
  }
  return person;
}
//利用工厂函数来创建对象
var person1 = createPerson("zhangsan", 18, 'male');
var person2 = createPerson("lisi", 20, 'female');
//var person3 = ...

工厂模式创建对象的优点是:只需要向工厂函数冲出啊如实参,工厂函数就会创建同一类型的对象。
但同时缺点也很明显:这种方式本质上是将创建对象的过程进行了封装,本质并没有改变,我们创建一个student时无法知道其具体的数据类型,只知道这是一个对象,往往实际开发中我们需要确定这个对象到底是个Person的实例还是Dog的实例。

2、构造函数模式

ECMAScript 中的构造函数是用于创建特定类型对象的。像 Object 和 Array 这样的原生构造函数,运行时可以直接在执行环境中使用。当然也可以自定义构造函数,以函数的形式为自己的对象类型定义属性和方法。

JavaScript中可以自定义构造函数,从而自定义对象类型的属性和方法,构造函数本身也是函数,只不过可以用来创建对象。

JS中构造函数和java中构造函数很像。

2.1 自定义构造函数

前面案例用构造函数可以这样写

// 自定义构造函数
function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.sayName = function () {
    console.log(this.name);
  }
}

与工厂模式的区别

在上面这个案例中,Person()构造函数代替了 createPerson()工厂函数。实际上,Person()内部的代码跟 createPerson()基本是一样的,只是有如下区别:

  • 没有显式地创建对象。
  • 属性和方法直接赋值给了 this。
  • 没有 return。

此外,注意函数名 Person 的首字母大写了。按照惯例,构造函数名称的首字母都是要大写的,非构造函数则以小写字母开头。这是从面向对象编程语言那里借鉴的,有助于在 ECMAScript 中区分构造函数和普通函数。毕竟 ECMAScript 的构造函数就是能创建对象的函数。

2.2 根据构造方法创建实例

//创建实例对象
var person1 = new Person('zhangsan', 29, 'male');
var person2 = new Person('lisi', 19, 'female');

(1) 在内存中创建一个新对象。
(2) 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
(3) 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
(4) 执行构造函数内部的代码(给新对象添加属性)。
(5) 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
person1 和 person2 分别保存着 Person 的不同实例。所有对象都会从它的原型上继承一个 constructor 属性,这两个对象的constructor 属性指向 Person。

console.log(person1.constructor === Person); // true 
console.log(person2.constructor === Person); // true
\```

2.3 函数表达式自定义构造函数

构造函数不一定要写成函数声明的形式。赋值给变量的函数表达式也可以表示构造函数

var Person = function (name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.sayName = function () {
    console.log(this.name);
  };
}
var person1 = new Person("zhangsan", 29, "male");
var person2 = new Person("lisi", 27, "female");
person1.sayName(); // zhangsan 
person2.sayName(); // lisi 
console.log(person1 instanceof Object); // true 
console.log(person1 instanceof Person); // true 
console.log(person2 instanceof Object); // true 
console.log(person2 instanceof Person); // true

在实例化时,如果不想传参数,那么构造函数后面的括号可加可不加。只要有 new 操作符,就可以调用相应的构造函数。

function Person() {
  this.name = "larry";
  this.sayName = function () {
    console.log(this.name);
  };
}
var person1 = new Person();
var person2 = new Person;

person1.sayName(); // larry 
person2.sayName(); // larry 
console.log(person1 instanceof Object); // true 
console.log(person1 instanceof Person); // true 
console.log(person2 instanceof Object); // true 
console.log(person2 instanceof Person); // true

2.4 确认对象类型的方式

instanceOf

使用instanceOf关键词来判断对象类型。
constructor 本来是用于标识对象类型的。不过,一般认为 instanceof 操作符是确定对象类型更可靠的方式。

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上或者说判断一个对象是某个对象的实例。

例如:

//instanceof 操作符的结果所示:
console.log(person1 instanceof Object); // true 
console.log(person1 instanceof Person); // true 
console.log(person2 instanceof Object); // true 
console.log(person2 instanceof Person); // true 

定义自定义构造函数可以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处。person1 和 person2 之所以也被认为是 Object 的实例,是因为所有自定义对象都继承自 Object。

2.5 构造函数也是函数

构造函数与普通函数唯一的区别就是调用方式不同。除此之外,构造函数也是函数。并没有把某个函数定义为构造函数的特殊语法。任何函数只要使用 new 操作符调用就是构造函数,而不使用 new 操作符调用的函数就是普通函数。

var Person = function (name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.sayName = function () {
    console.log(this.name);
  };
}
// 作为构造函数
var person = new Person("Jacky", 29, "male");
person.sayName(); // Jacky

// 作为函数调用
Person("lisi", 27, "female"); // 添加到全局对象 node global 浏览器 window
global.sayName(); // lisi

//在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "wangwu", 25, "male");
o.sayName(); // wangwu

如果没有使用 new 操作符调用 Person(),结果会将属性和方法添加到全局对象。
在调用一个函数而没有明确设置 this 值的情况下(即没有作为对象的方法调用,或者没有使用call()/apply()调用),this 始终指向 Global 对象(在浏览器中就是 window 对象)。
在另一个对象的作用域中通过call()调用函数,同时将特定对象指定为作用域。这里的调用将对象 o 指定为 Person()内部的 this 值,因此执行完函数代码后,所有属性和 sayName()方法都会添加到对象 o 上。

2.6 构造函数模式带来的缺点

构造函数虽然有用,但也不是没有问题。构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。也就是说person1 和 person2 都有名为 sayName()的方法,但这两个方法不是同一个 Function 实例,因为每次定义函数时,都会初始化一个对象
所以构造函数实际上是这样的

function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.sayName = new Function("console.log(this.name)"); // 逻辑等价
}

因此不同实例上的函数虽然同名却不相等。如下:

console.log(person1.sayName == person2.sayName); // false

因为都是做一样的事,所以没必要定义两个不同的 Function 实例。况且,this 对象可以把函数与对象的绑定推迟到运行时。要解决这个问题,可以把函数定义转移到构造函数外部。

function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.sayName = sayName; //
}
function sayName() {
  console.log(this.name);
}
var person1 = new Person("zhangsan", 29, "male");
var person2 = new Person("lisi", 27, "female");
person1.sayName(); // zhangsan 
person2.sayName(); // lisi

分析:
sayName()被定义在了构造函数外部。在构造函数内部,sayName 属性等于全局 sayName()函数。因为这一次 sayName 属性中包含的只是一个指向外部函数的指针,所以 person1 和 person2共享了定义在全局作用域上的 sayName()函数。

但由此又带来了一个问题,全局作用域被搞乱了。因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法,那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。
新产生的问题可以通过原型模型来解决

3、原型模式

3.1 简介

每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。
实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。
原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。

例如:

function Person(){}
Person.prototype.name = "zhangsan";
Person.prototype.age = 29;
Person.prototype.gender = "male";
Person.prototype.sayName = function () {
  console.log(this.name);
};
var person1 = new Person();
person1.sayName(); // zhangsan 
var person2 = new Person();
person2.sayName(); // zhangsan 
console.log(person1.sayName == person2.sayName); // true

这里,所有属性和 sayName()方法都直接添加到了 Person 的 prototype 属性上,构造函数体中什么也没有。但这样定义之后,调用构造函数创建的新对象仍然拥有相应的属性和方法。与构造函数模式不同,使用这种原型模式定义的属性和方法是由所有实例共享的。因此 person1 和 person2 访问的都是相同的属性和相同的 sayName()函数。

3.2 原型层级

在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发
现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在
原型对象上找到属性后,再返回对应的值。

因此,在调用 person1.sayName()时,会发生两步搜索。首先,JavaScript 引擎会问:“person1 实例有 sayName 属性吗?”答案是没有。然后,继续搜索并问:“person1 的原型有 sayName 属性吗?”答案是有。于是就返回了保存在原型上的这个函数。在调用person2.sayName()时,会发生同样的搜索过程,而且也会返回相同的结果。这就是原型用于在多个对象实例间共享属性和方法的原理。

虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值。如果在实例上添加了一个与原型对象
中同名的属性,那就会在实例上创建这个属性,这个属性会遮住原型对象上的属性。例如:

function Person() { }
Person.prototype.name = "zhangsan";
Person.prototype.age = 29;
Person.prototype.gender = "male";
Person.prototype.sayName = function () {
  console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "lisi";
console.log(person1.name); // lisi,来自实例
console.log(person2.name); // zhangsan,来自原型

在这个案例中,person1 的 name 属性遮蔽了原型对象上的同名属性。虽然 person1.name 和person2.name 都返回了值,但前者返回的是"lisi"(来自实例),后者返回的是"zhangsan"(来自原型)。当 console.log()访问 person1.name 时,会先在实例上搜索个属性。因为这个属性在实例上存在,所以就不会再搜索原型对象了。而在访问 person2.name 时,并没有在实例上找到这个属性,所以会继续搜索原型对象并使用定义在原型上的属性。可以通过hasOwnProperty()可以查看访问的是实例属性还是原型属性。

只要给对象实例添加一个属性,这个属性就会遮蔽(shadow)原型对象上的同名属性,也就是虽然不会修
改它,但会屏蔽对它的访问。即使在实例上把这个属性设置为 null,也不会恢复它和原型的联系。不过,使用 delete 操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象。

function Person() { }
Person.prototype.name = "zhangsan";
Person.prototype.age = 29;
Person.prototype.gender = "male";
Person.prototype.sayName = function () {
  console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

// 通过hasOwnProperty()可以查看访问的是实例属性还是原型属性
console.log(person1.hasOwnProperty('name')); //false

person1.name = "lisi";
console.log(person1.name); // lisi,来自实例
//只在重写 person1 上 name 属性的情况下才返回 true,表明此时 name 是一个实例属性,不是原型属性
console.log(person1.hasOwnProperty('name')); //true

console.log(person2.name); // zhangsan,来自原型

console.log(person2.hasOwnProperty('name'));//false

delete person1.name;
console.log(person1.name); // zhangsan,来自原型

console.log(person1.hasOwnProperty('name'));//false

这个修改后的案例中使用 delete 删除了 person1.name,这个属性之前以"lisi"遮蔽了原型上的同名属性。然后原型上 name 属性的联系就恢复了,因此再访问 person1.name 时,就会返回原型对象上这个属性的值。
在这里插入图片描述
person1.name = ‘lisi’
在这里插入图片描述
delete person1.name
在这里插入图片描述

3.3 原生对象的原型

1)什么是原生对象?

JS拥有强大的类型系统,主要包括原生对象、宿主对象和浏览器拓展对象。
  原生对象分为两类:原始类型和对象类型。原始类型又分为两类,一类是空值,一类是包装对象;对象类型也可以分为两类:一类是构造器对象,一类是单体内置对象。

(1)原始类型
1、空值(2种)

分别是undefined和null。逻辑上,undefined表示原始类型的空值,null表示对象类型的空值。

2、包装对象(3种)

字符串类型string(对应包装类型String)、数字类型number(对应包装类型Number)、布尔类型boolean(对应包装类型Boolean)

(2)对象类型
1、构造器对象(9种)

包括对象Object、函数Function、数组Array、日期Date、错误Error、正则RegExp。
当显示的使用new 构造器函数来定义包装对象,那么字符串String、数字Number、布尔值Boolean也属于构造器对象

2、单体内置对象(4种)

Math、JSON、全局对象Window、arguments对象,他们不需要声明或者构造器构造,可以直接使用。

2)原生对象的原型?

所有原生引用类型的构造函数(包括 Object、Array、String 等)都在原型上定义了实例方法。
比如,数组实例的 sort()方法就是 Array.prototype 上定义的,而字符串包装对象的 substring()方法也是在 String.prototype 上定义的,如下所示:

console.log(typeof Array.prototype.sort); // "function" 
console.log(typeof String.prototype.substring); // "function"

通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法。可以像修改自定义对象原型一样修改原生对象原型,因此随时可以添加方法。
例如:给String原始包装类型的实例添加一个last()方法

//给字符串添加属性或方法  要写到对应的包装对象的原型下才行
var str = 'hello';
String.prototype.last = function () {
  // 返回指定位置的字符
  return this.charAt(this.length - 1);
};
console.log(str.last()); // o 

如果给定字符串调用 last()方法,那么该方法会返回 给定字符串的最后一个字符。因为这个方法是被定义在String.prototype 上,所以当前环境下所有的字符串都可以使用这个方法。str是个字符串,在读取它的属性时,后台会自动创建 String 的包装实例,从而找到并调用 last()方法。

尽管可以这么做,但并不推荐在产品环境中修改原生对象原型。这样做很可能造成误会,而且可能引发命名冲突。另外还有可能意外重写原生的方法。

3.4 简化原型模式的写法

在前面的案例中,每次定义一个属性或方法都会把 Person.prototype 重写一遍。为了减少代码冗余,也为了从视觉上更好地封装原型功能,直接通过一个包含所有属性和方法的对象字面量来重写原型成为了一种常见的做法。
例如:

function Person() {}

Person.prototype = {
  name: "zhangsan",
  age: 29,
  gender: "male",
  sayName() {
    console.log(this.name);
  }
};

var person1 = new Person()
console.log(person1.constructor === Person); //false
console.log(person1.constructor === Object); //true

在这个案例中,Person.prototype 被设置为等于一个通过对象字面量创建的新对象。
最终结果是一样的,只有一个问题
这样重写之后,Person.prototype 的 constructor 属性就不指向 Person了。通常在创建函数时,也会创建它的 prototype 对象,同时会自动给这个原型的 constructor 属性赋值。而上面的写法完全重写了默认的 prototype 对象,因此其 constructor 属性也指向了完全不同的新对象(Object 构造函数),不再指向原来的构造函数。那么又该怎么解决这个问题呢?

可以在重写原型对象时,专门设置constructor的值

但由此方式恢复 constructor 属性会创建一个[[Enumerable]]为 true 的属性。而原生 constructor 属性的特[[Enumerable]]特性为false,也就是默认不可枚举的。

因此,可以通过Object.defineProperty()方法来定义 constructor 属性,该方法可以修改属性特性。

如下:

function Person() { }
Person.prototype = {
  //这种方式恢复 constructor 属性会默认创建一个[[Enumerable]]为 true 的属性
  //constructor: Person,
  name: "zhangsan",
  age: 29,
  gender: "male",
  sayName() {
    console.log(this.name);
  }
};
// 更改constructor的指向,使其指向实例对应的构造构造函数,不改的话默认指向了Object
Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false, // 将constructor改为非枚举属性
  value: Person
});
var person1 = new Person()
console.log(person1.constructor == Person); //true
console.log(person1.constructor == Object); //false

3.5 原型模式带来的问题

原型模式也不是没有问题。首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。虽然这会带来不便,但还不是原型的最大问题。原型的最主要问题源自它的共享特性

我们知道,原型上的所有属性是在实例间共享的,这对函数来说比较合适。此外如前面案例中所示,可以通过在实例上添加同名属性来简单地遮蔽原型上的属性。真正的问题来自包含引用值的属性。看如下案例

function Person() { } //构造函数
//构造函数指向的原型对象
Person.prototype = {
  constructor: Person,
  name: "zhangsan",
  friends: ["lisi", "wangwu"],
  sayName() {
    console.log(this.name);
  }
};

//创建实例对象
var person1 = new Person();
var person2 = new Person();

//给person1对象中的friends数组添加元素
person1.friends.push("zhaoliu");

console.log(person1.friends); // [ 'lisi', 'wangwu', 'zhaoliu' ]
console.log(person2.friends); // [ 'lisi', 'wangwu', 'zhaoliu' ]

console.log(person1.friends === person2.friends); // true

分析:这里,Person.prototype 有一个名为 friends 引用类型的属性,它包含一个字符串数组。然后这里创建了两个Person 的实例。person1.friends 通过 push 方法向数组中添加了一个字符串。由于这个friends 属性存在于 Person.prototype 而非 person1 上,新加的这个字符串也会在(指向同一个数组的)person2.friends 上反映出来。如果这是有意在多个实例间共享数组,那没什么问题。但一般来说,不同的实例应该有属于自己的属性副本。这就是实际开发中通常不单独使用原型模式的原因。

组合模式

组合使用构造函数模式和原型模式。构造函数用于定义实例属性,原型模式用于定义方法和共享属性。这种模式是目前在ECMAScript中使用最广泛,认同度最高的一种创建自定义类型的方法。如下:

//构造方法模式用来定义实例属性(非共享)
function Person(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
  this.firends = ['zhangsan', 'lisi'];
}

//原型模式用于定义方法和共享属性
Person.prototype = {
  constructor: Person,
  sayName: function () {
    console.log(this.name);
  }
};
var p1 = new Person('larry', 44, 'male');
var p2 = new Person('terry', 39, 'male');

p1.firends.push('robin');

console.log(p1.firends); // [ 'zhangsan', 'lisi', 'robin' ]
console.log(p2.firends); // [ 'zhangsan', 'lisi' ]
console.log(p1.firends === p2.firends); // false
console.log(p1.sayName === p2.sayName); // true

console.log(p1);
// Person {
//     name: 'larry',
//     age: 44,
//     gender: 'male',
//     firends: [ 'zhangsan', 'lisi', 'robin' ]
// }

构造函数的参数中传入一个引用类型的数据时…

// 构造函数
function Person(name,age,arr){
    this.name = name
    this.age = age
    this.arr = arr // 引用类型
    this.numberArr = ["hello","world"]
}

// 原型模式,公共属性和方法
Person.prototype = {
    sayName(){
        console.log(this.name,this.age);
    },
}

// 在原型上创建constructor指向构造函数
Object.defineProperty(Person.prototype,'constructor',{
    enumerable:false,
    value:Person, // 将constructor指向Person构造函数
})

// 如果传入的是一个相同的引用类型,那么还是会起到变量共享的效果,因为他们访问的是同一块内存地址,如下:
// let arr = [1,2,3]
// let person1 = new Person("张三",23,arr)
// let person2 = new Person("李四",33,arr)
// person1.arr.push(4)
// console.log(person1.arr) // [1,2,3,4]
// console.log(person2.arr) // [1,2,3,4]

// 所以最好是直接传入数据,而不是一个引用对象,如下
let person3 = new Person("张安",24,[1,2,3])
let person4 = new Person("里斯",25,[1,2,3])
// 这样操作的就是各自数据了
person3.arr.push(4)
console.log(person3.arr) // [1,2,3,4]
console.log(person4.arr) // [1,2,3]
  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
嵌入式编程设计模式是指在嵌入式系统开发,采用一定的规范和模式来组织代码和设计系统架构的方法。它可以提高代码的可维护性和可重用性,降低开发过程的错误率和开发时间。常见的嵌入式编程设计模式有观察者模式、状态机模式工厂模式等。 在嵌入式系统开发,C语言是一种常用的编程语言。C语言具有高效、灵活、可移植等特点,非常适合嵌入式系统的开发。嵌入式系统的核心功能往往是由C语言编写的,因此熟练掌握C语言编程是嵌入式系统开发者的基本要求。 对于初学者而言,学习C语言编程可以通过读相关的书籍来入门。有许多经典的C语言教材,如《C Primer Plus》、《C程序设计语言》等,都是非常适合初学者的书籍。通过阅读这些书籍,可以系统地了解C语言的基本语法、变量、数据类型、控制语句等,并通过实例掌握C语言的编程技巧。 除了书籍,还可以通过查阅计算机相关的PDF文档来学习编程。计算机技术发展很快,新的技术、工具和语言不断涌现,对于学习者来说,及时获取最新的信息非常重要。通过查阅计算机相关的PDF文档,可以了解最新的编程技术、开发工具等,扩展自己的知识面,并提升自己的编程能力。 总而言之,嵌入式编程设计模式、C语言编程和计算机PDF文档是嵌入式系统开发者的学习和提高的重要资源。通过学习这些内容,可以掌握嵌入式系统开发的基本技能,并不断提升自己的编程能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值