JS原型理解——JS中的原型对象

JavaScript中的原型对象

下一篇JS原型理解——JS继承的实现方式

原型

原型是JavaScript中继承的基础,JavaScript的继承就是基于原型的继承。

一 理解原型

1.1 函数的原型对象

  • 无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象)。
  • 默认情况下,所有原型对象自动获得一个名为constructor的属性,指回与之相关的构造函数。
// 声明一个函数
function Person() {}
// 声明之后,函数就有了一个与之关联的原型对象
console.log(typeof Person.prototype);
console.log(Person.prototype);

下面是代码的运行结果:
运行结果

下面可以看出Person函数和Person.prototype之间的关系:

理解原型1

1.2 对象实例的原型对象

  • 每次调用构造函数创建一个新实例,这个实例内部的[[Prototype]]指针就会被赋值为构造函数的原型对象。
  • 脚本中没有访问这个[[Prototype]]特性的标准方式,但是Firefox、Safari和Chrome会在每个对象上暴露__proto__属性。
//声明构造函数Person后,函数就有了一个与之关联的原型对象
function Person(){ 
} 
//给Person关联的原型对象添加属性和方法
//这些属性和方法为各个对象实例所共享
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function(){ 
 alert(this.name); 
}; 
//调用Person构造函数,创建对象实例
//对象实例调用原型对象中共享的方法
var person1 = new Person(); 
person1.sayName(); //"Nicholas" 
var person2 = new Person(); 
person2.sayName(); //"Nicholas" 
alert(person1.sayName == person2.sayName); //true 

下图展示了上面例子各个对象之间的联系:

理解原型2

说明:

  • 构造函数、原型对象和实例是3个完全不同的对象(在js中,函数也是对象)。
  • 实例通过内部的隐藏特性[[Prototype]]链接到原型对象(在某些浏览器中通过__proto__访问该特性),构造函数通过prototype属性链接到原型对象。实例与构造函数没有直接联系,与原型对象有直接联系。
  • 同一个构造函数创建的多个实例,共享同一个原型对象。
  • 正常的原型链会终止于Object的原型对象,Object原型的原型是null。

关于说明的最后一点,请看下面的代码:

function Person() {}
console.log(Person.prototype.__proto__ === Object.prototype);//true
console.log(Person.prototype.__proto__.constructor === Object);//true
console.log(Person.prototype.__proto__.__proto__ === null);//true

二 原型层级

2.1两步搜索

在通过对象访问属性时,会按照这个属性名开始搜索。

  • 搜索开始于对象实例本身。如果在这个实例上发现了给定的属性名,则返回对应的属性值。
  • 如果没有找到这个属性,则搜索会沿着指针进入原型对象。在原型对象上找到属性后,再返回属性值。
注意:前面提到的constructor属性只存在于原型对象,因此通过实例对象也是可以访问到的。
function Person() {}
Person.prototype.name = "Jack";
Person.prototype.age = 29;
Person.prototype.job = "Soft Engineer";
Person.prototype.sayName = function() {
    console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();

person1.hobby = "play basketball";
console.log(person1.hobby);//play basketball: 在实例上搜索到
console.log(person1.name);//Jack:在原型上搜索到
console.log(person2.hobby);//undefined:在实例和原型都没有搜索到

2.2属性遮蔽

只要给对象实例添加一个属性,这个属性就会遮蔽原型对象上的同名属性。

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

person1.name = "Greg";
console.log(person1.name);// "Greg":来自实例
console.log(person2.name);//"Jack":来自原型

2.3hasOwnProperty()

hasOwnProperty()方法用于确定某个属性是在实例上还是在原型对象上。这个方法是继承自Object的,会在属性存在于调用它的对象实例上时返回true。

function Person () {
		
	}
Person.prototype.name = "Jack";
var p1 = new Person();
p1.sex = "男";
//sex属性是直接在p1属性中添加,所以是true
alert("sex属性是对象本身的:" + p1.hasOwnProperty("sex"));
//name属性是在原型中添加的,所以是false
alert("name属性是对象本身的:" + p1.hasOwnProperty("name"));
//age属性不存在,所以也是false
alert("age属性是存在于对象本身:" + p1.hasOwnProperty("age"));

三 原型和in操作符

3.1in操作符

in操作符会在可以通过对象访问指定属性时返回true,无论该属性是否在实例上还是在原型上。

function Person(){ 
} 
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function(){ 
 alert(this.name); 
}; 
var person1 = new Person(); 
var person2 = new Person(); 
alert(person1.hasOwnProperty("name")); //false 
alert("name" in person1); //true 
person1.name = "Greg"; 
alert(person1.name); //"Greg" ——来自实例
alert(person1.hasOwnProperty("name")); //true 
alert("name" in person1); //true 
alert(person2.name); //"Nicholas" ——来自原型
alert(person2.hasOwnProperty("name")); //false 
alert("name" in person2); //true 
delete person1.name; 
alert(person1.name); //"Nicholas" ——来自原型
alert(person1.hasOwnProperty("name")); //false 
alert("name" in person1); //true 

3.2 hasOwnProperty()和 in 操作符配合

在以上代码执行的整个过程中,name 属性要么是直接在对象上访问到的,要么是通过原型访问到的。因此,调用"name" in person1 始终都返回 true,无论该属性存在于实例中还是存在于原型中。

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

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

function Person(){ 
} 
Person.prototype.name = "Nicholas"; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function(){ 
 alert(this.name); 
}; 
var person = new Person(); 
alert(hasPrototypeProperty(person, "name")); //true 
person.name = "Greg"; 
alert(hasPrototypeProperty(person, "name")); //false 

四 创建对象的两种模型

对于创建对象来说,我们希望的是尽可能像其他语言,比如java中的那样,每个对象实例有自己独一份的属性,但是方法最好是多个对象实例可以共享,不造成内存浪费。而下面介绍的js中创建对象的两种方法都有其缺陷。

4.1 构造函数模型创建对象的缺陷

在构造函数中添加的属性和方法,每个对象都有自己独有的一份,大家不会共享。这个特性对属性比较合适,但是对方法又不太合适。因为对所有对象来说,他们的方法应该是一份就够了,没有必要每人一份,造成内存的浪费和性能的低下。

function Person() {
	    this.name = "李四";
	    this.age = 20;
	    this.eat = function() {
	        alert("吃完东西");
	    }
}
var p1 = new Person();
var p2 = new Person();
//每个对象都会有不同的方法
//也就是说每调用一次构造函数,都会重复创建一遍方法
alert(p1.eat === p2.eat); //fasle

可以使用下面的方法解决:

<script>
	function Person() {
	    this.name = "李四";
	    this.age = 20;
	    this.eat = eat;
	}
  	function eat() {
	    alert("吃完东西");
    }
	var p1 = new Person();
	var p2 = new Person();
	//因为eat属性都是赋值的同一个函数,所以是true
	//将方法的定义从构造函数提取出来,可以避免重复创建同一个方法
	alert(p1.eat === p2.eat); //true
</script>

但是上面的这种解决方法具有致命的缺陷:封装性太差。使用面向对象,目的之一就是封装代码,这个时候为了性能又要把代码抽出对象之外,这是反人类的设计。

4.2 原型模型创建对象的缺陷

原型中的所有的属性都是共享的。也就是说,用同一个构造函数创建的对象去访问原型中的属性的时候,大家都是访问的同一个对象,如果一个对象对原型的属性进行了修改,则会反映到所有的对象上面。

但是在实际使用中,每个对象的属性一般是不同的。张三的姓名是张三,李四的姓名是李四。

但是,这个共享特性对方法(属性值是函数的属性)又是非常合适的。所有的对象共享方法是最佳状态。这种特性在c#和Java中是天生存在的。

4.3使用组合模式解决上述两种缺陷

原型模式适合封装方法,构造函数模式适合封装属性,综合两种模式的优点就有了组合模式。

<script type="text/javascript">
	//在构造方法内部封装属性
	function Person(name, age) {
	    this.name = name;
	    this.age = age;
	}
	//在原型对象内封装方法
	Person.prototype.eat = function (food) {
		alert(this.name + "爱吃" + food);
	}
	Person.prototype.play = function (playName) {
		alert(this.name + "爱玩" + playName);
	}
    
	var p1 = new Person("李四", 20);
	var p2 = new Person("张三", 30);
	p1.eat("苹果");
	p2.eat("香蕉");
	p1.play("Jack");
	p2.play("Mike");
</script>

4.4动态原型模式创建对象

​ 前面讲到的组合模式,也并非完美无缺,有一点也是感觉不是很完美。把构造方法和原型分开写,总让人感觉不舒服,应该想办法把构造方法和原型封装在一起,所以就有了动态原型模式。

​ 动态原型模式把所有的属性和方法都封装在构造方法中,而仅仅在需要的时候才去在构造方法中初始化原型,又保持了同时使用构造函数和原型的优点。

请看下面的代码:

<script type="text/javascript">
	//构造方法内部封装属性
	function Person(name, age) {
		//每个对象都添加自己的属性
	    this.name = name;
	    this.age = age;
	    /*
	    	判断this.eat这个属性是不是function,如果不是function则证明是第一次创建对象,
	    	则把这个funcion添加到原型中。
	    	如果是function,则代表原型中已经有了这个方法,则不需要再添加。
	    	perfect!完美解决了性能和代码的封装问题。
	    */
	    if(typeof this.eat !== "function"){
	    	Person.prototype.eat = function () {
	    		alert(this.name + " 在吃");
	    	}
	    }
	}
	var p1 = new Person("志玲", 40);
	p1.eat();	
</script>

五 原型的其他注意点

5.1 其他原型语法

  • 可以使用对象字面量来重写原型,而不需要将属性和方法逐个赋值给原型对象。
  • 但是要注意,如果不在其中重写constructor属性,则默认等于Object。
function Person() {}
Person.prototype = {
    name: "Jack",
    age: 29,
    job: "Software Engineer",
    sayName() {
        console.log(this.name);
    }
};
let friend = new Person();
console.log(friend.constructor == Person); //false
console.log(friend.constructor == Object);//true

最好是在重写原型对象时专门设置一下它的值。

function Person() {
    
}
Person.prototype = {
	constructor: Person,
    name: "Jack",
    age: 29,
    job: "Software Engineer",
    sayName() {
        console.log(this.name);
    }
};
//以这种方式恢复constructor属性会创建一个[[Enumerable]]为true的属性
//而原生constructor属性默认是不可枚举的。
//因此,最好使用Object.defineProperty()方法定义constructor属性:
function Person() {}
Person.prototype = {
    name: "Jack",
    age: 29,
    job: "Software Engineer",
    sayName() {
        console.log(this.name);
    }
};
Object.defineProperty(Person.prototype,"constructor",{
    enumerable: false,
    value: Person
});

5.2 原型的动态性

  • 因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。
let friend = new Person();
Person.prototype.sayHi = function() {
    console.log("hi");
}
friend.sayHi(); //"hi"
  • 实例对象的[[Prototype]]指针是在调用构造函数时自动赋值的,这个指针即使吧原型修改为不同的对象也不会改变。重写整个原型会切断最初原型与构造函数的联系,但是实例引用的仍然是最初的原型。
function Person() {}
let friend = new Person();
Person.prototype = {
    constructor: Person,
    name: "Jake",
    age: 29,
    job: "Software Engineer",
    sayName() {
        console.log(this.name);
    }
};
friend.sayName(); //错误

原型动态性

5.3 原生对象原型

  • 所有原生引用类型的构造函数都在原型上定义了实例方法。比如数组实例的sort()方法就是Array.prototype上定义的,而字符串的substring()方法也是在String.prototype上定义的。
  • 通过原生对象的原型可以取得所有默认方法的引用,也可以给原生类型的实例定义新的方法。
  • 可以像修改自定义对象原型一样修改原生对象原型,但是不推荐这么做。
String.prototype.startsWith = function(text) {
    return this.indexOf(text) ===0 ;
};
let msg = "Hello world!";
console.log(msg.startsWith("Hello")); //true

5.4原型的问题

  • 首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。
  • 原型的最主要问题源自它的贡献性。
  • 原型上的所有属性是在实例间共享的,这对函数来说比较合适。对于包含原始值的属性也还好,如前面例子所示,可以通过实例上添加同名属性来简单遮蔽原型上的属性。真正的问题来自包含引用值的属性。看下面的例子。
function Person() {}
Person.prototype = {
    constructor: Person,
    name: "Jake",
    age: 29,
    job: "Software Engineer",
    friends: ["Shelby","Court"],
    sayName() {
        console.log(this.name);
    }
};
let p1 = new Person();
let p2 = new Person();
p1.friends.push("Van");
console.log(p1.friends);// "Shelby,Court,Van"
console.log(p2.friends);// "Shelby,Court,Van"
console.log(p1.friends==p2.friends);//true
//如果是有意在多个实例间共享数组,那没什么问题
//但是一般来说,不同的实例应该有自己的属性副本

下一篇JS原型理解——JS继承的实现方式

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值