【JS】面向对象之原型基础

426 阅读8分钟

一、原型的概念

JS红皮书介绍原型的第一句话:

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

这句话介绍了以下几个要点:

  1. 函数(只要是个函数),都有prototype属性,且这个属性是个指针
  2. prototype属性指向的对象,其实就是我们常说的原型对象
  3. 原型对象中,包含了一些属性、方法,可以被特定类型的实例专门使用

还是这个例子:

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()
person1.sayName()   //Nicholas

var person2 = new Person()
person1.sayName()   //Nicholas

console.log(person1.sayName == person2.sayName)  //true

原型指向

创建了两个实例person1,person2,他们分别指向了Person的原型对象Person Prototype。

  1. 原型对象Person Prototype有一个默认的constructor属性,指向Person本身。
  2. 原型对象Person Prototype有一个名字叫sayName的函数,还有其他name,age,job三个属性。
  3. person1作为一个实例,调用方法sayName,本质上调用的是Person Prototype中的sayName。

所以,可得结论:

  1. 原型对象默认只有constructor属性,且默认指向构造函数本身,其他方法继承于Object
  2. 构造函数的实例,默认有一个指针,指向了原型对象,这个指针是不可见的。(在Firefox、Safari、Chrome中,这个指针是__proto__,所以person1.__proto__指向了原型对象)
  3. 当获得实例的属性时,如果实例中找到了指定的属性,直接返回其值;如没有找到,会顺着其指针去原型对象中寻找。

问:访问一个实例的原型对象,途径有哪些?

答:没有正式的途径用于直接访问一个实例的原型对象(原型链中的“连接”被定义在一个内部属性中,在 JavaScript 语言标准中用 [[prototype]] 表示)

然而,大多数现代浏览器还是提供了一个名为 proto (前后各有2个下划线)的属性,其包含了原型对象。

obj.__proto__ 

obj.constructor.prototype 

Object.getPrototypeOf(obj) 

上面三种方法之中,前两种都不是很可靠。 最新的ES6标准规定,__proto__属性只有浏览器才需要部署,其他环境可以不部署。而obj.constructor.prototype在手动改变原型对象时,可能会失效(详细在下面原型对象的覆盖进行解释)。

图示如下:

二、原型的相关操作和in操作符

补充:

  1. 使用delete可以删除实例的属性,但是不会删除原型对象的同名属性
function Person(){}
Person.prototype.name = 'wangwu'

var person1 = new Person()
var person2 = new Person()

person1.name = 'zhangsan'
console.log(person1.name)   //zhangsan
console.log(person2.name)   //wangwu

delete person1.name
delete person2.name     //person2没有自己的name属性,进行删除也不会影响原型对象中的name属性
console.log(person1.name)   //wangwu
console.log(person2.name)   //wangwu
  1. hasOwnProperty()检验一个属性是否存在于某个实例中

这个例子非常好,不再赘述

function Person(){}
Person.prototype.name = 'wangwu'

var person1 = new Person()
var person2 = new Person()

alert(person1.hasOwnProperty("name"))   //false   person1中name属性来自原型,返回false
person1.name = 'zhangsan'
alert(person1.hasOwnProperty("name"))   //false   person1中name属性来自其本身,返回true

alert(person2.hasOwnProperty("name"))   //false   person2中name属性来自原型,返回false

delete person1.name
alert(person1.hasOwnProperty("name"))   //false   person1中name属性来自原型,返回false
  1. in 操作符 和hasOwnProperty()进行区分,in操作符只要通过对象能访问到属性,就返回true。in操作符不包含原型链上的属性,也不能操作不可枚举的属性。
语法: '属性名' in 实例  //返回true或者false
  1. 得到原型对象可枚举的实例属性(constructor属性是不可枚举的)
function Person(){}
Person.prototype.name = 'zhangsan'
Person.prototype.sayName = function(){
    console.log(this.name)
}
console.log(Object.keys(Person.prototype));  //name,sayName
  1. 得到原型对象所有实例属性(constructor属性是不可枚举的)
function Person(){}
Person.prototype.name = 'zhangsan'
Person.prototype.sayName = function(){
    console.log(this.name)
}
console.log(Object.getOwnPropertyNames(Person.prototype));  //constructor,name,sayName
  1. 原型对象的覆盖

使用对象字面量给原型对象赋值,重新创建了一个原型对象,但是新原型对象的constructor指向了Object构造函数,不再指向Person构造函数

function Person(){}
Person.prototype = {
    name : 'zhangsan',
    sayName : function(){
        console.log(this.name)
    }
}

可以显式修改constructor指向,使其重新指向Person,但是这会使constructor属性变成可枚举

function Person(){}
Person.prototype = {
    constructor:Person,
    name : 'zhangsan',
    sayName : function(){
        console.log(this.name)
    }
}

而当constructor属性可枚举,原型对象即可打印,打印出的Person.prototype如下:

Person {
  constructor: [Function: Person],
  name: 'zhangsan',
  sayName: [Function: sayName] }

如果想继续保持constructor属不可枚举,可以使用defineProperty()函数,如下:

Object.defineProperty(Person.prototype,"constructor",{
    enumerable:false,
    value:Person
})

三、原型语法与动态性

先看一个例子

1   function Person(){}
2   var friend = new Person()
3   Person.prototype = {
4       constructor:Person,
5       name:'Nicholas',
6       age:29,
7       job:'Software Engineer',
8       sayName:function(){
9            alert(this.name)
10      }
11  }
12  friend.sayName()  //error

这里报错是因为,在第二行给friend声明并赋值时,指针指向如下:

所以friend在被赋值的时候指向的原型对象中,是没有sayName方法的,这就解释了为什么会报错。

而在对Person.prototype进行重新赋值时,指针指向如下:

重新创建了一个新的原型对象,将Person构造函数的prototype属性,指向了新的原型对象

个人理解(可忽略):

JS里面的原型、原型对象的设计,其实类似于JAVA中类和构造函数。JS的构造方法类似于JAVA中的类;JS中的原型对象类似于JAVA中的构造方法;重写原型类似于重写JAVA构造方法;JS重写原型对象之后,构造函数不再指向默认的原型对象,JAVA重写了类的构造方法之后,默认的构造方法也不可用了。

包括新增ES6里面的class和constructor,个人认为也借鉴了JAVA中类的机制,这里不再展开。

再看一个例子

function Person(){}
var friend = new Person()
Person.prototype.name = 'zhangsan'
Person.prototype.sayName = function () {
    console.log(this.name)
}

friend.sayName()    //zhangsan

Person.prototype = {
    constructor:Person,
    name:'Nicholas',
    age:29,
    job:'Software Engineer',
    sayName:function(){
        alert(this.name)
    }
}

friend.sayName()    //zhangsan

var friend2 = new Person()
friend2.sayName()   //Nicholas

问:为什么friend.sayName()两次输出的都是zhangsan?

答:因为声明并赋值friend的时候,Person构造函数的指向的原型对象(这里简称原型对象A,下同),原型对象A内,存储了属性name,值是zhangsan;还存储了属性sayName,是一个函数;所以在第一次调用friend.sayName()的时候,输出了zhangsan;

之后对Person.prototype进行赋值,创建了新的原型对象(这里简称原型对象B,下同),但是并未改变friend实例默认指向,换言之,friend仍旧指向原型对象A,所以依然输出zhangsan

问:为什么friend2.sayName()输出的是Nicholas?

答:因为重新创建了原型对象(原型对象B)之后,才对friend2进行声明和赋值,由于Person.prototype已经指向了原型对象B,新的Person实例friend2也就默认指向了原型对象B

再来一个例子

先了解一个概念:instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。下面是ECMAScript-262中对instanceof关键字进行定义的伪代码:

function instance_of(L, R) {    //L 表示左表达式,R 表示右表达式
 var O = R.prototype;   // 取 R 的显示原型
 L = L.__proto__;       // 取 L 的隐式原型
 while (true) { 
   if (L === null) 
     return false; 
   if (O === L)     // 这里重点:当 O 严格等于 L 时,返回 true 
     return true; 
   L = L.__proto__; 
 } 
}

JS这里是借鉴了JAVA的,如出一辙,这是JAVA8对instanceof语言规范的链接:docs.oracle.com/javase/spec…

function F(){};
function B(){
    this.name='zhangsan'
}

F.prototype = new B()
var foo = new F();

console.log(foo.constructor)    //[Function: B]
console.log(foo instanceof B)   //true
console.log(foo instanceof F)   //true
console.log(foo.name)   //zhangsan

创建了构造函数B的匿名实例new B(),重写了F的原型对象,这时F原型对象里面的属性已经是new B()的属性了,new B()是一个实例,不具有constructor属性,所以重写F原型对象也没有了constructor属性。

但是,具有了访问构造函数B的原型对象、以及构造函数B中属性的能力,所以构造函数B在foo的原型链上,foo instanceof B返回true。

是不是觉得,F像是一个儿子(子类型),B像是一个父亲(父类型),儿子获得了父亲的一些身体外貌特征(F具有访问B的属性能力),其实这就是简单的继承了,所以JS的继承是基于原型链的

其原型链大致如下图:

个人理解(可忽略):

这里类似于JAVA中,父类引用指向子类对象,子类获得了父类一部分属性,实现了继承

四、JS原生对象的原型

这里以Array这个JS原生对象举例,我们在控制台执行下面的代码

typeof Array  

结果输出了:"function"

继续在控制台执行下面的代码:

console.log(Array.prototype)

结果输出了一个数组,数组中包含了Array这个引用类型中的全部方法。这个数组就是Array引用类型的原型对象

总结一下得出结论:

Array本身是一个函数,它的原型对象是一个数组,数组中包含了若干方法,所以Array.prototype.XX(),就是在调用Array原型对象的XX()方法。

我们再来看一下,下面的代码:

var mycars = new Array("Saab","Volvo","BMW")
console.log(mycars.join('--'))      //Saab--Volvo--BMW

创建了一个Array引用类型的实例mycars,mycars的指针默认指向了函数Array的原型对象,原型对象中有一个叫join()的方法,所以mycars可以调用这个方法,返回了Saab--Volvo--BMW

其实我们也可以通过编程给Array的原型对象添加一些方法,像下面这样,每创建Array的实例,都可以调用自定义方法zhangsanFunction(不建议这么做,自定义和自带方法容易引起混淆和命名冲突)

Array.prototype.zhangsanFunction = function(){
        console.log("我叫张三!!!")
}

var mycars = new Array()
mycars.zhangsanFunction();      //我叫张三!!!

个人理解(可忽略):

联想一下Vue封装插件,就是给Vue的原型对象上,加了一个自定义函数,实现的一处封装,处处可用。

Thanks for Reading......