浅谈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
这里用图解会清晰很多:
原型模式的问题:
实例默认都取得相同的属性值
共享特性(原型上的所有属性是在实例间共享的)
继承
在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
上面这条蓝色链子就是原型链
了!
一般地,原型链的尽头就是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参考: