js 创建对象的模式

130 阅读7分钟
1.工厂模式

工厂模式其实就是一个函数接收参数然后经过一系列的处理返回了一个处理后的东西.

function createPerson(name, age, job) { 
    let o = new Object(); 
    o.name = name; 
    o.age = age; 
    o.job = job;
    o.sayName = function() { console.log(this.name); }; return o; 
} 
let person1 = createPerson("Nicholas", 29, "Software Engineer"); 
let person2 = createPerson("Greg", 27, "Doctor");

工厂函数其实挺好用的,但是有个缺点就是他没有类型,因为返回的这个对象是new的Object,很单纯

2.构造函数模式

构造函数的首字母应该大写

function Person(name, age, job){ 
    this.name = name; 
    this.age = age; 
    this.job = job; 
    this.sayName = function() { 
        console.log(this.name); 
    }; 
} 
let person1 = new Person("Nicholas", 29, "Software Engineer"); 
let person2 = new Person("Greg", 27, "Doctor"); 
person1.sayName(); 
// Nicholas 
person2.sayName(); 
// Greg

*构造函数也可以赋值给变量去用*

let Person = function(name, age, job) { 
    this.name = name; 
    this.age = age; 
    this.job = job; 
    this.sayName = function() { 
        console.log(this.name); 
    }; 
}
let person1 = new Person("Nicholas", 29, "Software Engineer");

他有三个区别于工厂函数

  1. 没有直接创建对象
  2. 直接把属性和方法给了this了
  3. 也没写return 如果要创建这个对象的实例,就得用new关键字,下面是new关键字触发的行为
  4. 在内存里创建个新的对象
  5. 把新对象的protoType赋值为构造函数的protoType
  6. 把构造函数里的this指向这个新对象
  7. 执行构造函数里的代码,给新对象里面加属性
  8. 如果构造函数返回非空对象了,就返回这个对象;否则就返回刚创建的新对象
  • 鉴定对象类型的两个方法
  1. 看看这个对象的构造函数constructor是不是等于一个函数,如果等于,就是这个类型
console.log(person1.constructor == Person); // true
  1. 都推荐用这个instanceof方法,
console.log(person1 instanceof Object); // true 
console.log(person1 instanceof Person); // true

从这里就能看出来构造函数对于工厂函数的优势了,就是人家构造函数是有家的,有归宿的,查类型能查到,相反工厂函数就属于克隆人,没爹没娘的,反正就是给几个参数就粗来了,也没法用上面的类型判断做亲子鉴定

  • 构造函数也是函数,也能直接调用,但是直接调用的话,函数里的this就指向全局了,就都给到windows对象了;当然也没人会闲着没事写构造函数调着玩,构造函数一般就是new用,或者用call之类的改变this指向去使用. **构造函数的毛病**
  • 构造函数里的属性没啥毛病,毛病出在方法里,构造函数里的方法在每一个实例里面都是新的,这些函数同名却不相等,构造函数里的方法(或者也叫函数都行)其实是这样存在的:
function Person(name, age, job){ 
    this.name = name; 
    this.age = age; 
    this.job = job; 
    this.sayName = new Function("console.log(this.name)"); // 逻辑等价 
}

这就很尴尬了家人们,他们都是干一个活儿,却实际上各自为战,这一点必要也没有,为了解决这个事情,曾经有人在外面定义方法,转移了

function Person(name, age, job){ 
    this.name = name; 
    this.age = age; 
    this.job = job; 
    this.sayName = sayName; 
} 
function sayName() { 
    console.log(this.name); 
}

但是这样好么?一点也不好!这岂不是得在外面定义一堆方法,还是在全局作用域里面,这就很乱了.然后就出现了下面的原型模式来解决这个痛点.

3.原型模式

每个函数都会建立一个prototype属性,这个属性是个对象,里面包含了一些方法和属性,这个对象就是调用构造函数创建的对象的原型,在它上面定义的属性和方法是被所有的实例所共享的

function Person() {} 
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function() { 
    console.log(this.name); 
}; 
let person1 = new Person(); 
person1.sayName(); 
// "Nicholas" 
let person2 = new Person(); 
person2.sayName(); 
// "Nicholas" 
console.log(person1.sayName == person2.sayName); 
// true

这里面关系比较绕,总体上围绕着三个东西来说:构造函数,原型,实例

  1. 实例和构造函数没有直接关系,和原型对象有直接关系
  2. 构造函数和原型二者相互联系,构造函数会产生原型,然后原型里面的constructor又会指回构造函数,二者就是这么勾结的
  3. 构造函数,原型对象,实例 是三个不同的对象
  4. 实例通过_proto_连接到原型
console.log(person1.__proto__ === Person.prototype); 
// true 
conosle.log(person1.__proto__.constructor === Person); 
// true
  • 可以通过isPrototypeOf()方法确定原型之间的关系,并且可以获取原型
console.log(Object.getPrototypeOf(person1) == Person.prototype); 
// true
  • 可以通过Object.create()来创 建一个新对象,同时为其指定原型
let biped = { numLegs: 2 }; 
let person = Object.create(biped); 
person.name = 'Matt'; console.log(person.name); 
// Matt 
console.log(person.numLegs); 
// 2 
console.log(Object.getPrototypeOf(person) === biped); 
// true

有关原型的层级

  • 访问对象的属性的时候,会按照属性的名称进行搜索,先查找实例自己,有的话就返回值,没有的话就会找到原型对象中,在原型中寻找,如果找到就返回值
  • 需要注意的是,虽然实例可以读取原型上的值,但是并没有修改原型上的值的能力,如果在实例上添加了和原型同名的属性,这个属性就会遮盖住原型上的属性.这个遮蔽可以使用delete进行消除,在消除之后实例就会继续去访问原型上的值了.
  • hasOwnProperty可以确定这个属性是不是属于实例自己,如果是就返回true,用来区分实例属性和原型属性
console.log(person1.hasOwnProperty("name"));
// false
  • hasPrototypeProperty正好相反,如果属性是原型属性,就返回true,否则返回false,不过他俩的调用方式还是有点差别的
person.name = "Greg"; 
console.log(hasPrototypeProperty(person, "name"));
// false
  • in操作符是用来确定一个属性在不在这个对象里的,他就比较随和,只要能访问到,就能返回true
console.log("name" in person1); 
// true

对象的迭代

  1. 简单的两个静态方法:Object.values()Object.entries()
  • Object.values()返回对象值的数组
  • Object.entries()返回对象键值对的数组
  • 非字符串属性会被转换为字符串输出
  • 符号属性会忽略
const o = { foo: 'bar', baz: 1, qux: {} }; 
console.log(Object.values(o));
// ["bar", 1, {}]
console.log(Object.entries((o))); 
// [["foo", "bar"], ["baz", 1], ["qux", {}]]

关于原型的定义

  • 之前定义原型的时候都是一个属性一个属性的定义,其实也可以直接通过字面量去定义,但是这样就会造成实例的构造函数变成object而不是原本的构造函数,这种字面量的定义实际上是重写
function Person() {} 
Person.prototype = { 
    name: "Nicholas", 
    age: 29, 
    job: "Software Engineer", 
    sayName() { 
        console.log(this.name); 
    } 
};
let friend = new Person(); 
console.log(friend instanceof Object); 
// true 
console.log(friend instanceof Person); 
// true
console.log(friend.constructor == Person); 
// false 
console.log(friend.constructor == Object); 
// true
  • 虽然说instanceof还是可以确定一下的,但是不能靠constructor进行识别了.
  • 原型具有动态性,实例和原型二者就是指针关系,如果原型发生变化了,那么实例也会跟着发生变化.就算在实例生成之后原型发生了变化,实例也能正常的接受这个变化
  • 重写整个原型会切断最初原型与构造函数的联系,但实例引用的仍然是最初的原型。 记住,实例只有指向原型的指针,没有指向构造函数的指针。
function Person() {} 
let friend = new Person(); 
Person.prototype = { 
    constructor: Person, 
    name: "Nicholas", 
    age: 29, 
    job: "Software Engineer", 
    sayName() { 
        console.log(this.name); 
    } 
}; 
friend.sayName(); // 错误

Person 的新实例是在重写原型对象之前创建的。在调用 friend.sayName()的时候,会导致错误。这是因为 firend 指向的原型还是最初的原型,而这个原型上并没有 sayName 属性。重写构造函数上的原型之后再创建的实例才会引用新的原型。而在此之前创建的实例仍然会引用最初的原型。

原型的问题

  1. 它弱化了向构造函数传递初始化参数的能力,会导致所有实例默 认都取得相同的属性值。
  2. 对于包含引用数据类型的属性,会造成不同实例使用相同数据的情况,实际上不同实例应该有自己的副本.
function Person() {} 
Person.prototype = { 
    constructor: Person, 
    name: "Nicholas", 
    age: 29, 
    job: "Software Engineer", 
    friends: ["Shelby", "Court"],
    sayName() { console.log(this.name); } 
}; 
let person1 = new Person(); 
let person2 = new Person(); 
person1.friends.push("Van"); 
console.log(person1.friends); 
// "Shelby,Court,Van"
console.log(person2.friends); 
// "Shelby,Court,Van" 
console.log(person1.friends === person2.friends); 
// true