写在前面
我们知道在面向对象编程的语言中,有一句统筹全局的中心句—”万物皆对象“,原型和原型链也是基于这个基础理解的。
对于初学js的继承机制—”原型“和”原型链“这两个概念的理论时,总是忘了记、记了忘。所以死记硬背真的是没得用的,得深入理解其背后的设计思想,得理解加记忆,如虎添翼。
至于为什么这样说,就随着这篇文章去揭开珍妮的面纱,如剥洋葱般去探究它的本质。
来不及解释,快上车。
JS继承的设计思想
我们知道创建对象有两种方式:一种是最常见的对象字面量,一种就是常说的通过new来创建对象实例。其实这两种方式描述的对象都是等价的,属性和方法都是一致的。
// 字面量对象
let obj = {
name:"yichuan",
age:18,
sayName(){
console.log("name: ", this.name);
}
}
// new创建对象实例
let obj2 = new Object();
obj2.name = "pingping";
obj2.sayName = function(){
console.log("name: ", this.name);
}
使用对象字面量或者Object构造函数可以轻松创建对象,但是在创建具有同样接口的对个对象时,会重复编写很多代码。那么,我们想可不可以创建一个容器,将共享的属性和方法存在里面,这样就可以在多个对象中使用。
在es6之前没有正式支持类和继承的结构,但是能够通过原型链继承进行模仿实现类和继承。事实上,es6的类也的确是封装了构造函数和原型继承的语法糖。
工厂模式
工厂模式可以用于抽象创建特定对象过程,解决创建多个类似对象的问题,但是没有解决对象标识的问题,不能设置新创建对象的类型。
// 工厂模式
function createPerson(name, age, city){
let obj = new Object();
obj.name = name;
obj.age = age;
obj.city = city;
obj.sayName = function(){
console.log("my name is : ", this.name);
}
return obj;
}
let preson1 = createPerson("yichuan",18,"BeiJing");
let preson2 = createPerson("onechuan",28,"GuangZhou");
构造函数模式
在js中构造函数是用于创建特定类型对象的,在前面使用了Object原生构造函数创建对象,运行时可以直接在执行环境中使用。但其实,我们也可以进行自定义构造函数,用函数的形式为自己的对象定义属性和方法。
// 构造函数模式
function Person(name, age, city){
this.name = name;
this.age = age;
this.city = city;
this.sayName = function(){
console.log(this.name);
}
}
let p1 = new Person("yichuan",18,"Beijing")
let p2 = new Person("onechuan",19,"Guangzhou")
p1.sayName()//yichuan
p2.sayName()//onechuan
构造函数模式相比于工厂模式而言,没有显式创建对象,属性和方法都是直接赋给this,也没有return返回任何值。
那么使用new构造函数的方式创建对象,具体会发生什么?
会有如下操作:
- 1)在内存中开辟新的空间创建新对象
- 2)这个新对象内部的proto特性被赋值为构造函数的prototype属性
- 3)构造函数内部的this指向新对象
- 4)给新对象添加属性
- 5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
构造函数是什么?
其实构造函数也是函数,和普通函数没啥区别,只是调用的方式不同而已,通过new调用的函数是构造函数。构造函数定义的方法会在每个实例上都创建一遍,每次定义函数时,都会初始化一个对象。
function Person(name, age, city){
this.name = name;
this.age = age;
this.city = city;
this.sayName = new Function("console.log(this.name);");
}
原型模式
每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法,而这个对象就是通过调用构造函数创建的实例对象的原型,那么这个对象就叫做原型对象。
原型对象的作用是:在原型对象上定义的属性和方法可以被对象实例所共享,即对象原型相当于一个存储公共属性和方法的容器。
等等,这不就是前面所说的构造器中直接赋值给对象实例的值吗?
其实不是,其实在进行构造函数Person定义时,构造函数内部是个空对象,没有任何属性和方法。然而,可以通过在Person的prototype上直接定义属性和方法,来挂载到Person对象的原型上。这样通过new Person()得到的对象实例是可以共享Person.prototype上的属性和方法。如下所示:
function Person(){}
Person.prototype.name = "yichuan";
Person.prototype.age = 18;
Person.prototype.city = "Beijing";
Person.prototype.sayName = function(){
console.log(this.name);
}
let p1 = new Person();
p1.sayName();
let p2 = new Person();
p2.sayName();
原型和原型链
上面的原型模式中,已经将原型和原型对象的概念引出来了,那么我们重新整理下思路:
构造函数的prototype属性指向的原型对象中,定义了所有实例对象都能够共享的属性和方法,而不需要共享的属性和方法则直接定义在构造函数上。
通过构造函数创建的实例对象,会自动拥有原型对象上共享的属性或方法。
function Person(name){
this.name = name;
}
Person.prototype.address = "earth";
const p = new Person("yichuan");//{name:age}
const p1 = new Person("onechuan");
在上面代码中,在构造函数Person的prototype原型上定义了一个公共属性 Person.prototype.address="earth";
,那么通过new出来的实例对象p和p1都会天生继承属性address,而p和p1各自的name值分别为"yichuan"
和"onechuan"
。
函数与对象的关系
- 函数是对象,对象都是通过函数创建的
- 函数与对象并不是简单的包含与被包含的关系
原型的类别
- 显式原型:prototype,是每个函数function独有的属性
- 隐式原型:proto,是每个对象都具有的属性
原型和原型链
- 原型:一个函数可以看做一个类,原型是所有类都有的一个属性,prototype原型的作用就是给这个类的每个对象都添加一个统一的方法
- 原型链:每个对象都有一个proto,它指向它的prototype原型对象;它的prototype原型对象又有一个proto,指向它的prototype原型对象,就这样向上查找原型,直到顶级对象Object.prototype,最终指向是null
用图片描述原型链:
我们看到原型链的最终归属都是对象,而Object.prototype
的_proto_
指向的是null,这是为了避免死循环而设置的,所以一切皆空。
参考文章
- 【重点】图解:告诉面试官什么是 JS 原型和原型链?
- 面不面试的,你都得懂原型和原型链
- 《Javascript高级程序设计》
写在最后
- 所有实例对象的proto都指向该构造函数的prototype原型对象 (即:
p._proto_ === Person.prototype
) - 所有函数(包括构造函数)都是
Function
的实例,所有函数的_proto_
都指向Function
的原型对象 - 所有的原型对象(包括
Function
的原型对象)都是Object
的实例,所以_proto_
都指向Object
(构造函数)的原型对象。而Object构造函数的_proto_
指向null
。 Function
构造函数本身就是Function
的实例,所以_proto_
指向Function
的原型对象。
最后的小结,摘自《【重点】图解:告诉面试官什么是 JS 原型和原型链?》一文。