原型模式在js中经常用来创建对象的一种模式,它并不是通过创建类来创建对象,而是通过克隆一个对象来创建对象。在js中没有类的概念,所以js中的面向对象编程方式基本都是使用原型模式实现的。
正文
1.原型对象
function Person(){};Person.prototype.name = 'Marys';以上代码声明了一个新函数Preson,每一个新函数里都包含一个prototype属性(这是一个指针),这个属性指向一个对象,这就是 "原型对象",这个对象包含构建该函数的一切属性和方法,其中包含一个constructor属性,该属性指向函数本身。如下图所示
var person1 = new Person();上面的代码中,person1声明为Person的实例,在类语言中就可以说,person1是Person类型,就像数字 1 是Number类型一样。但是js中没有类的概念,那么js是怎么实例化一个对象的呢?首先,当一个实例对象被声明,该实例便会包含一个[[prototype]]属性(这是也一个指针),在一些浏览器中,他可以用 __proto__ 属性名读取到;然后,这个[[prototype]]属性会指向他的父级的原型对象————prototype,换句话说prototype也是person1的原型对象。关系如下图所示。
1.1 原型链
如上图,便是在chrome中打印person1的结果。可以看到,在person1下的__proto__
属性展开后是Person的原型,其中也包含一个 __proto__
属性,这个原型便是指向最根部的Object原型。像这样一层包一层的结构就像是一条链子,把原型一个个连接起来,这就是原型链。当要调用一个对象或一个方法的时候,在创建的实例中找不到的话,js便会沿着这条原型链一直向下找,如果原型链中都找不到,就是undefined了。如下面的代码,打印出来的是Person中定义的Marys
function Person(){};Person.prototype.name = 'Marys';var person1 = new Person();console.log(person1.name);
1.2 关于this
this的指向是根据**执行上下文(作用域)**决定的,是一个由构造函数constructor创建的对象,所以this中的属性方法并不存在原型对象中。具体this的变化过程如下:- 创建实例对象
- 将作用域赋予对象(因此this就指向了新对象)
- 执行构造函数中的代码,为实例对象添加属性和方法
小结
js中的原型模式实际上就是用来实现继承的一种方式,但是使用原型模式继承的属性和方法都有一个缺点:对于引用类型值得属性是会共用的。也就是说继承的属性如果是引用类型值不是独立的,当改变其中一个实例的属性值时,其他同父级的实例的该属性也是会改变。如下代码,当person1的numbers属性推入一个值,person2中的numbers值也会受影响。但是如果代码是person1.numbers = [1,2,3]
,person2的numbers属性是不会受到影响的,因为这样相当于person1声明了一个新的属性numbers,这是属于person1自己的属性,用this关键字是能读取到的,当声明属性与__proto__
中的属性重名的时候,会遮挡原型对象中的属性。
function Person(){};Person.prototype.numbers = [1,2,3,4];var person1 = new Person();var person2 = new Person();person1.numbers.push(5);console.log(person2.numbers);//[1,2,3,4,5]person1.numbers = [1,2,3];console.log(person2.numbers);//[1,2,3,4,5]console.log(person1);/*Person {numbers: Array(3)} numbers:(3) [1, 2, 3] __proto__: numbers:(5) [1, 2, 3, 4, 5] constructor:ƒ Person() __proto__:Object*/