# ES5构造函数模式和原型模式创建对象

在掘金平台看到若川大佬画的图挺直观可理解的,就拿来仅供自己学习 img

# 构造函数模式

例子:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function () {
        console.log(this.name)
    }
}
Person.test = function() {
    console.log('这是静态方法')
}
var person1 = new Person('linjiaheng', 18, 'qd')
var person2 = new Person('linjiaheng', 18, 'qd')
console.log(person1 === person2) // false
console.log(Function.prototype === Function.__proto__) // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

要创建一个新实例,必须使用new操作符,以这种方式来创建特定类型的对象实际上会经历以下四个步骤:

  1. 创建一个新对象(为该对象开辟一块属于它的内存空间)
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象),新对象内部属性[[Prototype]](非正式属性__proto__)连接到构造函数的原型
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

即使传入的参数一样,每个实例都是不相等的

# constructor(构造函数)属性

每个实例对象都有一个constructor属性,该属性直接指向构造函数,上述例子中则指向Person

console.log(person1.constructor === Person) // true
1

对象的构造函数属性最初是用来标识对象类型的。但是instanceof操作符要更可靠些。

console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
1
2

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方(创建对象)

# 构造函数其他调用方式

上述的例子都是通过new操作符来创建新的实例的,那么可以不通过该操作符来调用构造函数吗? 任何函数可以通过new操作符来调用的则为构造函数,不能的则为普通函数。

  • 当在全局作用域中调用一个函数时,this对象总是指向Global对象(在浏览器则为window对象,在node环境下的则为global对象),多个构造函数的话则执行最后一个的。
Person('linjiaheng', 18, 'qd');
window.sayName(); // linjiaheng
1
2
  • 也可以使用call()或者apply(),在某个特殊对象的作用域中调用Person()函数。
var o = new Object();
Person.call(o, 'jone', 25, 'nurse');
o.sayName(); // jone
1
2
3

# 改进构造函数

正如:

console.log(person1 === person2) // false
1

所以可以知道每个实例的方法都是不一样的,每创建一个实例就要重新创建一个方法(这里为sayName()),函数也是对象,等同于重新实例化了一个对象。所以上述的例子可以这样改进:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}
function sayName() {
    console.log(this.name)
}
1
2
3
4
5
6
7
8
9

改进之后的弊端就是如果对象需要定义很多方法,那么就要定义很多个全局函数,后面将通过原型模式来解决

# 原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针指向一个原型对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。构造函数就变成空函数了,不过依然可以通过调用构造函数的方式来新建对象。

上述的例子就变成了:

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

var person1 = new Person();
var person2 = new Person();
console.log(person1.sayName == person2.sayName) // true  此处和构造函数不一样

console.log(p.__proto__ === Person.prototype)  // true
console.log(Person.__proto__ === Function.prototype) // true
console.log(Person.prototype.__proto__ === Object.prototype)  // true 因为所有的对象都是直接或间接继承自Object
console.log(Object.prototype.__proto__)  // null  因为Object作为最顶层对象,是原型链的最后一环.所以这里的null表示了Object.prototype没有原型

console.log(Person.__proto__ === Function.prototype) // true
console.log(Person.__proto__ === Object.prototype) // false

Array.isArray(Array.prototype) // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 理解原型对象

在默认情况下,所有原型对象都会自动获得一个constructor构造函数属性,这个属性是一个指向prototype属性所在函数的指针

console.log(Person.prototype.constructor === Person) //  true
1
  1. 创建自定义的构造函数之后,其原型对象默认只会取得constructor属性。
  2. 当调用构造函数创建一个新实例之后,该实例当内部将包含一个指针(内部属性),指向构造函数的原型对象。该指针可以叫为[[Prototype]]或者__proto__(隐式原型,所有对象(除了null)都有这个内部属性)
  3. 虽然所有实现中都无法访问到[[Prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。
alert(Person.prototype.isPrototypeOf(person1)) // true
1

ES5中增加Object.getPrototypeOf()方法。访问对象的 obj.__proto__ 属性,默认走的是 Object.prototype 对象上 __proto__ 属性的 get/set 方法。

console.log(Object.getPrototypeOf(person1) === Person.prototype) // true
console.log(Object.getPrototypeOf(person1).name) // Nicholas

function f() {}
const a = f.prototype,b = Object.getPrototypeOf(f)
console.log(a === b);// false
// f.prototype 是使用使用 new 创建的 f 实例的原型. 而 Object.getPrototypeOf 是 f 函数的原型.
// a === Object.getPrototypeOf(new f()) // true
// b === Function.prototype // true
1
2
3
4
5
6
7
8
9

第一行代码只是确定该方法返回的对象实际就是该对象的原型;第二个代码就是验证,这在继承知识点中很有用

多个实例共享原型所保存的属性和方法的基本原理: 当代码读取某个对象的某个属性,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索实例的原型对象,在原型对象中继续查找。

# 不能修改原型中的属性

如果我们在实例中添加一个属性,该属性与实例原型中的一个属性同名,那我们在实例中创建该属性,该属性将会屏蔽原型中的那个属性。因为实例中已经有属性了,就不用往原型去搜索

person1.name = "linjiaheng";
console.log(person1.name) // linjiaheng
console.log(person2.name) //Nicholas 来自原型
1
2
3

使用delete操作符就可以删除实例中的属性,从而能够重新访问原型的属性

delete person1.name
console.log(person1.name) // Nicholas 来自原型
1
2

使用hasOwnProperty()方法可以检测一个属性是否存在实例中,还是存在于原型中。只在给定属性存在于对象实例中时才会返回true

# 更简单的原型语法

减少Person.prototype代码量(用一个包含所有属性和方法的对象字面量重写原型对象)

function Person() {}
Person.prototype = {
    name: 'Nicholas',
    age: 29,
    job: 'software engineer',
    sayName: function () {
        alert(this.name)
    }
}
1
2
3
4
5
6
7
8
9

这样的写法有个例外就是实例对象的constructor属性不再指向原型

var person1 = new Person()
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person1.constructor == Person) //false
console.log(person1.constructor == Object) // true
1
2
3
4
5

改进后的原型写法其实相当于重新写了默认的prototype对象,因此constructor属性也变成新对象的(指向了Object构造函数) 可以通过重新设置constructor属性指向原型对象,不过这样该属性就变成可枚举的了。

# 原型对象的问题

  1. 省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。
  2. 原型中所有属性是被很多实例共享的。通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就突出了。

# 原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来---即使是先创建实例后修改原型也如此。例子:

var friend = new Person()
Person.prototype.sayHi = function() {
    console.log('hi')
}
friend.sayHi(); // 'hi
1
2
3
4
5

这种情况原因在于实例和原型之间的松散关系 但是如果是重写了整个原型对象,情况就不一样了。调用构造函数会为实例添加一个指向最初原型的[[Prototype]]指针,把原型修改为另外一个对象就等于切断构造函数与最初原型之间的联系

var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
 n: 2,
 m: 3
}

var c = new A()

console.log(b.n); // 1
console.log(b.m); // undefined

console.log(c.n) // 2
console.log(c.m) // 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会自己的一份实例属性的副本,但同时又共享着对方的引用,最大限度地节省了内存。
还可以向构造函数传递参数。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ['Shelby', "Count"];
}
Person.prototype = {
    constructor: Person,
    sayName: function() {
        console.log(this.name)
    }
}
var person1 = new Person('nichas', 29, 'software engineer')
var person2 = new Person('greg', 27, 'doctor');
person1.friends.push('linjiaheng')
console.log(Person.prototype.constructor === Person) // true
console.log(person1.friends);// ["Shelby", "Count", "linjiaheng"]
console.log(person2.friends);// ["Shelby", "Count"]
console.log(person1.sayName === person2.sayName); // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 寄生构造函数模式

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

function Person(name, age, job) {
    var o = {}
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        alert(this.name);
    };
    return o;
}
var friend = new Person('Nicholas', 29, "software engineer")
friend.sayName(); // Nicholas
1
2
3
4
5
6
7
8
9
10
11
12

这种模式很像工厂函数模式。构造函数在不返回值的时候,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。 返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有不同。为此不能依赖instanceof操作符来确定对象类型

# 稳妥构造函数模式

function Person(name, age, job) {
    var o = new Object(); // 创建要返回的对象
    o.sayName = function() {
        alert(name);
    };
    return o;
}
1
2
3
4
5
6
7

# 构造函数和原型模式的联系

function Fn() {
    this.x = 100
    this.y = 200
    this.getX = function() {
        console.log(this.x)
    }
} // 构造函数模式
Fn.prototype = {
    c: 10,
    y: 400,
    getX: function() {
        console.log(this) // Fn.prototype
        console.log(this.x)
    },
    getY: function() {
        console.log(this.y)
    },
    sum: function() {
        console.log(this.x + this.y)
    }
} // 原型模式
var f1 = new Fn()
var f2 = new Fn()
console.log(f1.y) // 200
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

img

  1. 如果构造函数和原型模式都对相同属性进行定义,采用构造函数模式的
console.log(f1.y)
console.log(f2)
console.log(f1.getX === f2.getX) // false
console.log(f1.getY === f2.getY) // true 原型模式和构造函数不一样的地方
console.log(f1.d === f2.d) // true
console.log(f1.__proto__.getY === Fn.prototype.getY) // true
console.log(f1.__proto__.getX === Fn.prototype.getX) // true
console.log(f1.__proto__.getX === f2.getX) // false
console.log(f1.__proto__.getY === f2.getY) // true
console.log(f1.constructor)
f1.__proto__.getX() // undefined
f2.getY() // 200
Fn.prototype.getY() // 400
f1.sum() // 300
Fn.prototype.sum() // undefined + 400 = NaN
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

针对上面例子分析;

  • 函数会开辟一个堆内存空间,存储执行函数里边的代码字符串
  • 所有类函数,天生自带一个属性prototype,这个属性是一个对象值,所以会开辟一个堆内存空间存储这个对象值。

# Javascript如何实现类,如何实例化

  1. 构造函数法

使用构造函数模拟类,在其内部用 this 关键字只带实例对象。

function Car(){
    this.brand = "Tesla";
}

// 生成实例的时候使用`new`关键字
var one_car = new Car();
console.log(one_car);

// 类的属性和方法,可以定义在构造函数的prototype对象之上。
Car.prototype.makeSound = function(){
    console.log("滴滴");
}
1
2
3
4
5
6
7
8
9
10
11
12
  1. Object.create()
var Car = {
    brand : "Tesla",
    makeSound : function(){console.log("滴滴")}
}

// 然后直接用Object.create()生成实例,不需要用到new
var one_car = Object.create(Car);
console.log(one_car.brand);//Tesla
one_car.makeSound();//滴滴

// 如果遇到浏览器不支持Object.create()可以自己模拟实现
if (!Object.create){
    Object.create = function(o){
        function F(){}
        F.prototype = o;
        return new F();
    };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这种方法比“构造函数方法“简单,但是不能实现私有属性和私有方法,实例对象之间也不能共享数据,对”类“的模拟不够全面。

  1. 极简主义法
// 封装
var Car = {
    createNew:function{
        var car = {};
        car.brand = "Tesla";
        car.makeSound = function(){console.log("滴滴");};
        return car;
    };
}

// 让一个类继承另一个类,实现起来很方便。只要在前者的createNew()方法中,调用后者的createNew()方法即可。

var Animal = {
    createNew: function(){
        var animal = {};
        animal.sleep = function(){console.log("sleep");};
        return animal;
    }
};

var Cat = {
    createNew:function(){
        var cat = Animal.createNew();
        cat.name = "大猫";
        cat.makeSound:function(){console.log("喵喵");};
        return cat;
    }
}

var cat1 = Cat.createNew();
cat1.sleep();//sleep
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

createNew() 方法中,只要不是定义在 cat 对象上的方法和属性,都是私有的。

var Cat = {
    createNew:function(){
        var cat={};
        var sound = "喵喵";
        cat.makeSound = function(){console.log(sound);};
        return cat;
    }
}

// 上面的内部变量sound,外部无法读取,只有通过cat的共有方法makeSound()的读取。
var cat1 = Cat.createNew();
console.log(cat1.sound);//undefined
1
2
3
4
5
6
7
8
9
10
11
12

有时候我们需要所有的实例对象能够读写同一项内部数据。这个时候,只要把这个内部数据封装在类对象的里面、createNew() 方法的外面即可。

var Cat = {
    sound : "喵喵",
    createNew: function(){
        var cat = {};
      cat.makeSound = function(){ alert(Cat.sound); };
      cat.changeSound = function(x){ Cat.sound = x; };
        return cat;
   }
};

var cat1 = Cat.createNew();
var cat2 = Cat.createNew();
cat1.makeSound();//喵喵

// 这是如果有一个实例对象,修改了共享的数据,另一个实例对象也会受到影响
cat2.changeSound("啦啦啦");
cat1.makeSound();//啦啦啦
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • ES6 class
class Car{
    constructor(brand){
        this.brand = brand;
    }
    makeSound(){
        console.log(this.brand);
    }
}

// 创建实例
let one_car = new Car("Tesla");
console.makeSound();

// 继承
class SmallCar extends Car{
    constructor(brand){
        super(brand);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19