JavaScript 面向对象之原型模式

🌙
手机阅读
本文目录结构

原型模式

在构造函数的理解图里

https://a.axihe.com/anbang/javascript/oop/oop-new-01.jpg

我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

如果按照字面意思来理解,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。

使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。

换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,

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

我们将 sayName() 方法和所有属性直接添加到了 Person 的 prototype 属性中,构造函数变成了空函数。

即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。

但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1 和 person2 访问的都是同一组属性和同一个 sayName() 函数。

理解原型

    1. 理解原型对象
    1. 判断某个属性属于原型,而不是私有属性;
    1. constructor 属性的指向问题
    1. 原型的动态性
    1. 原生对象的原型
    1. 原型模式的问题

1. 理解原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。

在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。就拿前面的例子来说,Person.prototype.constructor 指向 Person 。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。

https://a.axihe.com/anbang/javascript/oop/oop-new-02.png

原型总结

  • 1、只要创建了一个新函数 Person,系统会自动为该函数创建一个 prototype 属性,Person.prototype 这个属性指向函数的原型对象。
  • 2、函数的原型对象里自动获得一个 constructor(构造函数)属性,Person.prototype.constructor 又指回了 Person;
  • 3、函数的原型对象里自动获得一个__proto__属性,默认指向 Object;
  • 4、Person 的实例 person1 自动获得__proto__属性,默认指向 Person.prototype;
  • 5、调用 person1 的方法或者属性,默认会在自己的内部找;如果找不到,会通过 person1.__proto__找到 Person.prototype;如果 Person.prototype 再找不到,会通过 Person.prototype.proto,一直找到 Object 这个基类;如果再找不到就会报 undefined 或者报错;
  • 6、只要创建了一个新函数 Person, 函数 Person 本身也会自动获得 Person.__proto__的属性,该属性指向函数这个类(anonymous),函数类的__proto__指向 Object;

具体的对应逻辑代码如下:

function Person007(){
	var name="Person007"
}
//1、只要创建了一个新函数 Person,系统会自动为该函数创建一个 prototype 属性,Person.prototype 这个属性指向函数的原型对象。
console.log(Person007.prototype);

//2、函数的原型对象里自动获得一个 constructor(构造函数)属性,Person.prototype.constructor 又指回了 Person;
console.log(Person007.prototype.constructor);

//3、函数的原型对象里自动获得一个__proto__属性,默认指向 Object;
console.dir(Person007.prototype.__proto__);

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

// 只要创建了一个新函数 Person, 函数 Person 本身也会自动获得 Person.__proto__的属性,该属性指向函数这个类(anonymous),函数类的__proto__指向 Object;
console.dir(Person.__proto__);

person1.sayName(); //Person prototype

//4、Person 的实例 person1 自动获得__proto__属性,默认指向 Person.prototype;
person1.__proto__.sayName();

//5、调用 person1 的方法或者属性,默认会在自己的内部找;如果找不到,会通过 person1.__proto__找到 Person.prototype;如果 Person.prototype 再找不到,会通过 Person.prototype.__proto__,一直找到 Object 这个基类;如果再找不到就会报 undefined 或者报错;
person1.__proto__.__proto__.sayName();

深入理解 person1 的查找原理;

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

console.log(person1.name);// 来自原型
person1.sayName();// 来自原型

person1.name="change name"
console.log(person1.name);// 来自实例;

delete person1.name// 使用 delete 操作符删除了 person1.name ,之前它保存的 "change name"值屏蔽了同名的原型属性。把它删除以后,就恢复了对原型中 name 属性的连接。因此,接下来再调用 person1.name 时,返回的就是原型中 name 属性的值了
console.log(person1.name);// 来自原型

hasOwnProperty 检测一个属性是不是私有属性

检测一个属性是不是私有属性(这个方法只在给定属性存在于对象实例中时,才会返回 true )

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

console.log(person1.name);// 来自原型
person1.sayName();// 来自原型
console.log(person1.hasOwnProperty("name"));//false

person1.name="change name"
console.log(person1.name);// 来自实例
console.log(person1.hasOwnProperty("name"));//true

delete person1.name
console.log(person1.name);// 来自原型
console.log(person1.hasOwnProperty("name"));//false

https://a.axihe.com/anbang/javascript/oop/oop-new-03.jpg

2. 判断某个属性属于原型,而不是私有属性

in 操作符

操作符 in:单独使用和在 for-in 循环中使用。在单独使用时, in 操作符会在通过对象能够访问给定属性时返回 true ,无论该属性存在于实例中还是原型中。

代码如下:

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

console.log(person1.name);// 来自原型
person1.sayName();// 来自原型
console.log(person1.hasOwnProperty("name"));//false
console.log("操作符 in","name" in person1);//true

person1.name="change name"
console.log(person1.name);// 来自实例
console.log(person1.hasOwnProperty("name"));//true
console.log("操作符 in","name" in person1);//true

delete person1.name
console.log(person1.name);// 来自原型
console.log(person1.hasOwnProperty("name"));//false
console.log("操作符 in","name" in person1);//true

同时使用 hasOwnProperty 和 in

同时使用 hasOwnProperty() 方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中,

function hasPrototypeProperty(object, name){
	return !object.hasOwnProperty(name) && (name in object);
}

由于 in 操作符只要通过对象能够访问到属性就返回 true ,

hasOwnProperty() 只在属性存在于实例中时才返回 true ,因此只要 in 操作符返回 true 而 hasOwnProperty() 返回 false ,就可以确定属性是原型中的属性

3. constructor 属性的指向问题

每添加一个属性和方法就要敲一遍 Person.prototype 。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象,如下面的例子所示

更好地封装原型

function Person(){
}
Person.prototype = {
	name : "Person prototype",
	age : 29,
	job: "WEB",
	sayName : function () {
		alert(this.name);
	}
};

constructor 指向

将 Person.prototype 设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外: constructor 属性不再指向 Person 了

function Person(){
}
Person.prototype = {
	name : "Person prototype",
	age : 29,
	job: "WEB",
	sayName : function () {
		alert(this.name);
	}
};

var Person1=new Person;
console.log(Person1);
console.dir(Person1.__proto__.constructor);

一般写了原型后,就会把函数指回他原来的类;

function Person(){
}
Person.prototype = {
	// constructor : Person,
	name : "Person prototype",
	age : 29,
	job: "WEB",
	sayName : function () {
		alert(this.name);
	}
};

var Person1=new Person;
console.log(Person1);
console.dir(Person1.__proto__.constructor);

constructor 的枚举

以这种方式重设 constructor 属性会导致它的 [[Enumerable]] 特性被设置为 true。

默认情况下,原生的 constructor 属性是不可枚举的

function Person(){
}
Person.prototype = {
	constructor : Person,
	name : "Person prototype",
	age : 29,
	job: "WEB",
	sayName : function () {
		alert(this.name);
	}
};

// 重设构造函数,只适用于 ECMAScript 5  兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
	enumerable: false,
	value: Person
});

var Person1=new Person;
console.log(Person1);
console.dir(Person1.__proto__.constructor);

4. 原型的动态性

  • 在某个类函数的原型上追加方法和属性;之前创建的的类函数实例,都可以获得新追加的属性和方法;

  • 当重写某个类函数的原型时,之前创建的类函数实例,无法获得重写后的方法和属性;

    function Person(){
    }
    Person.prototype = {
    	constructor : Person,
    	name : "Person prototype",
    	age : 29,
    	job: "WEB",
    	sayName : function () {
    		alert(this.name);
    	}
    };
    var person1=new Person;
    console.log(person1.addTestKey);//undefined
    Person.prototype.addTestKey="test value";
    console.log(person1.addTestKey);//test value
    

如果换一种写法,下面这种的,就无法获得重写原型后的属性了;

function Person(){
}
var person1=new Person;
Person.prototype = {
	constructor : Person,
	name : "Person prototype",
	age : 29,
	job: "WEB",
	sayName : function () {
		alert(this.name);
	}
};
console.log(person1.addTestKey);//undefined
Person.prototype.addTestKey="test value";
console.log(person1.addTestKey);//undefined

原理是;因为 person1 创建的时候,里面的 person1.__proto__, 指向的是默认的 Person.prototype;新改变后的 prototype 已经改变了;而访问 person1.addTestKey 的时候,访问的是老地址中的这个属性;

5. 原生对象的原型

var testAry=[1,2,3]
console.log(testAry.__proto__==Array.prototype);//true

原生的 Array 类就是上面的这种思路;testAry 这个实例的__proto__,指向数组类的 prototype

也可以通过修改原生对象的原型,来追加方法的;

String.prototype.startsWith = function (text) {
	return this.indexOf(text) == 0;
};
var msg = "Hello world!";
console.log(msg.startsWith("Hello")); //true
console.log(msg.startsWith("world")); //false

6. 原型模式的问题

  • 因为原型的动态性,导致实例之间的属性和方法会互相干扰;

所有的属性都是定义在原型上的,所有的实例都会继承原型上的属性和方法;

function Person(){
}
Person.prototype = {
constructor : Person,
name : "Person prototype",
testAry:["test1","test1"]
};
var person1=new Person;
var person2=new Person;

console.log(person1.testAry,person2.testAry);
// 思考,为什么此时输出的时候,person1.testAry 还会有 test-push-person1;而不是 ["test1","test1"] 呢?
person1.testAry.push("test-push-person1");

改变 person1 的 testAry 后,就会影响到 person2 的 testAry 的属性;

表现如下

https://a.axihe.com/anbang/javascript/oop/oop-new-04.png

原型模式的弊端

这就是原型模式的弊端

不同的实例之间需要有自己的单独的私有属性和方法,并不能都是老共的!如果每个人的东西都是大家伙的,那就乱套了;上面的构造函数模式中,总结的是构造函数中,每个实例的中的内部属性都是相互独立的,为了解决公用的方法才引入原型模式的概念;这就引申到了公认最佳的创建类的方式构造函数模式 + 原型模式

思考题:为什么下面 person1 改变后,并不会影响 person 的变化;

function Person(){
}
Person.prototype = {
constructor : Person,
name : "Person prototype",
age : 29,
job: "WEB",
sayName : function () {
	console.log(this.name);
}
};
var person1=new Person;
var person2=new Person;

person1.age+=2;
person1.sayName=null;

console.log(person1.age,person1.sayName);
console.log(person2.age,person2.sayName);

AXIHE / 精选资源

浏览全部教程

面试题

学习网站

前端培训
自己甄别

前端书籍

关于朱安邦

我叫 朱安邦,阿西河的站长,在杭州。

以前是一名平面设计师,后来开始接接触前端开发,主要研究前端技术中的JS方向。

业余时间我喜欢分享和交流自己的技术,欢迎大家关注我的 Bilibili

关注我: Github / 知乎

于2021年离开前端领域,目前重心放在研究区块链上面了

我叫朱安邦,阿西河的站长

目前在杭州从事区块链周边的开发工作,机械专业,以前从事平面设计工作。

2014年底脱产在老家自学6个月的前端技术,自学期间几乎从未出过家门,最终找到了满意的前端工作。更多>

于2021年离开前端领域,目前从事区块链方面工作了