浅谈JavaScript原型

对象是JavaScript中很重要的一种基本数据类型,我们都知道对象是若干属性的无序集合(广义上的集合),每个对象除了维护自己的属性集外,还需继承另一个对象的prototype属性(即Prototype原型)。

对于开始总结该知识点的时候,信心还是满满的,但是到了现在开始正式整理后,不禁吐槽—这都是啥玩意啊? 咋越整理越迷糊。 😪

还是从创建对象开始吧。

创建对象

JS的创建对象方法总体上分为三种。

  • 工厂模式

  • 构造函数

  • 原型模式

工厂模式

这个工厂模式是一个封装了创建函数的函数,看以下代码:

function createCat(name, age){
    let o = new Object();
    o.name = name;
    o.age = age;
    o.sayHi = function() { console.log("Miao~" + o.name);
    return o;
}

这是一个创建对象的方法,无可知类型(新创建的对象是什么类型,即使我们源码知道为Object)。

构造函数模式

在其他编程语言,我们都知道构造函数是用来初始化对象的。

function Cat(name = "orange", age){
    this.name = name;
    this.age = age;
    sayHi(){ console.log("Miao~ " + this.name);
}
let orange = new Orange("orange", 3);
orange.sayHi(); // => "Miao~ orange"

相比工厂模式,构造函数内部没有显式创建对象,属性和方法赋予给了this,没有return语句。

上面用new操作符调用构造函数会默认执行以下操作:

  • 在内存中创建一个新对象

  • 这个对象内部的[[ Prototype ]]特性被赋值为构造函数的prototype属性

  • 构造函数内部的this被赋值为这个新对象

  • 执行构造函数内部的代码

构造函数也可以是函数表达式的形式:

let Cat = function(name, age) {
    this.name = name;
    this.age = age;
    this.sleep = function(){console.log(this.name + " zzz")};
    // 等价于
    // new Function("console.log(this.name + ' zzz')");
}
let threeColors = new Cat("threeColors", 2);
threeColors.sleep(); // => "threeColors zzz"

在JS中几乎所有函数(箭头函数、生成器函数和异步函数除外)都可是是构造函数,而且首字母大写不是非必须的。这意味着构造函数也是能被当作普通函数调用:

Cat("liHua", 5); // 此时this指向的是window全局对象,属性和方法将被添加到全局作用域
window.sayHi(); // => "Miao~ liHua"
console.log(age); // => 5

构造函数问题:

因为使用new操作符会创建新的对象实例,this的值指向不同的实例,因此每一个实例的属性和方法都是不一样的,这样会导致构造函数的方法(即对象)在调用时都会创建一个Function对象。我们知道,在其他面向对象的编程语言会用继承来解决,而JS采用的是原型。

原型模式

在JavaScript中,类的唯一标识是原型对象。

只有函数对象拥有prototype属性(但是箭头函数bind()函数返回的绑定函数没有)。

在创建函数时,函数会自动拥有一个名为prototype的属性,而prototype属性的值是一个对象(把这个对象称为Prototype原型),我们就可以说通过new操作符调用构造函数而创建的对象的原型是上面的那个对象(即指向prototype属性),而原型包含了构造函数创建的对象实例共享的属性和方法,默认情况下原型对象(Prototype)会自动获得一个名为constructor的属性,其反向指回与之关联的构造函数。

而且,每次调用构造函数创建的对象(实例)内部的[[ Prototype ]]指针会被赋值为构造函数的原型对象。JS脚本没有可以访问[[ Prototype ]]属性的方式,但 Firefox、Safari 和 Chrome 会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型( 虽然不推荐)。

可以得出:

实例与构造函数原型有直接关系,但是实例和构造函数之间有没有直接关系。

看以下代码:

function Animal(){};
Animal.prototype.sleep = function() {
    console.log("sleeping...")
};
Animal.prototype.eat() = function() {
    console.log("eatting...");   
}

//"继承特性"
let cat = new Animal();
cat.sleep(); // => "sleeping..."
let dog = new Animal();
dog.sleep(); // => "sleeping..."

//prototype属性
console.log(typeof Animal.prototype);	// => 'object'
console.log(Animal.prototype); // => {costructor: f Animal(), __proto__: Object}

//constructor属性
console.log(Animal.prototype.constructor === Animal); // => true

//__proto__ ([[ Prototype ]])
console.log(Animal.protoype === cat.__proto__); // => ture 

这里用图解会清晰很多:

prototype.png

原型模式的问题:

  • 实例默认都取得相同的属性值

  • 共享特性(原型上的所有属性是在实例间共享的)

继承

在JS中实现继承的方法是原型链

上图中实例通过[[ Prototype ]]这个特征指向原型对象查找相应的属性和方法,这个本质就是JS中的继承了。ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。

看以下代码:

// 定义父类
function SuperType() {
    this.property = true;
}
// 父类原型对象的方法
SuperType.prototype.getSuperValue = function() {
    return this.property;
}
//定义子类
function SubType() {
    this.subProperty = false;
}
// 将父类的实例对象赋值给子类构造函数的prototype属性,即继承
SubType.prototype = new SuperType();
// 子类原型对象的方法
SubType.prototype.getSubValue = function() {
    return this.subProperty;
}

let instance = new SubType();
console.log(instance.getSuperValue()); // => true

prototype-chain.pngprototype-chain.png

上面这条蓝色链子就是原型链了!

一般地,原型链的尽头就是Object.prototype.__proto__ ,值为null,代表没有对象。然后几乎所有对象的原型都是Object()的prototype属性。

Array.prototype.__proto__ === Object.prototype; // => true
Date.prototype.__proto__ === Object.prototype; // => true
String.prototype.__proto__ === Object.prototype; // => true
...
console.log(Object.prototype.__proto__); // null

继承关系判断:

使用instanceof操作符或isPrototypeOf()判断原型和实例的关系。

// instanceof
instance instanceof Object; // => true
instance instanceof SuperType; // => true
instance instanceof SubType; // => true
//isPrototypeOf()
Object.prototype.isPrototypeOf(instance); // => true
SuperType.prototype.isPrototypeOf(instance); // => true
SubType.prototype.isPrototypeOf(instance); // => true

//关于constructor的值
console.log(instance.__proto__.constructor); // => ƒ SuperType() {
                                         //			this.property = true;
                                        //	   }
// instance本身是没有constructor属性的,它会沿着原型链查找
console.log(instance.constructor); // =>  ƒ SuperType() {
                                //			this.property = true;
                                //	  }

instanceof和isPrototypeOf()判断原则是整条原型链里有没有构造函数的原型

注意:

对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链。

以下:

function SuperType() { 
 this.property = true; 
} 
SuperType.prototype.getSuperValue = function() { 
 return this.property; 
}; 
function SubType() { 
 this.subproperty = false; 
}
// 继承 SuperType 
SubType.prototype = new SuperType(); 
// 通过对象字面量添加新方法,这会导致上一行无效 (指的是Subtype和SuperType之间没了关系)
SubType.prototype = { 
 getSubValue() { 
 return this.subproperty; 
 }, 
 someOtherMethod() { 
 return false; 
 } 
}; 
let instance = new SubType(); 
console.log(instance.getSuperValue()); // 出错!

这时覆盖后的原型是一个 Object 的实例,而不再是 SuperType 的实例。

实例的变量查找

关于查找规则,JS的都大同小异(作用域),先从实例自身查找,自身没有找到就沿着原型链一直查找,直到查找为止或者原型的值为null了(Object.prototype.__proto__)。

小结

  • 创建函数时一定会有prototype属性

  • prototype属性的值是一个对象,称为Prototype(原型),一般指的是构造函数的原型

  • Prototype原型对象默认会有一个constructor属性,值为构造函数的引用

  • 实例对象内部有一个指针[[ Prototype ]]指向原型(Prototype)

  • 原型之间通过[[ Prototype ]]指针相连成原型链

Reference参考: