前言
原型和原型链是JavaScript这门语言中比较重要的机制,本篇会将原型和原型链由浅到深的进行讲解。
何为原型,何为原型链,何为原型对象
这里直接先说原型和原型链是什么。
- 原型是 JavaScript 对象相互继承特性的机制。
- 原型对象是js对象本身属性[[prototype]]指向的一个对象
- 原型链:对象有自己的原型对象,而原型对象也是一个对象,也有自己的[[prototype]]属性指向的原型对象,这样一直通过[[prototype]]属性不断往上查找原型对象就形成了一个链一样将这些对象串起来就是原型链。
原型
我们先看下面一段代码:
var obj = {
name:"aaa"
}
console.log(obj)//{name: 'aaa'}
console.log(obj.toString())//[object Object]
上面代码中我们可以看到,定义了一个对象obj,obj内属性我们通过控制台打印出来只能看到一个name属性,但我们却依然可以调用toString,那么这个方法从哪里来呢?我们在控制台继续把这个对象展开:
可以看到展开后多了一个属性,[[prototype]],这个属性也是一个对象类型的数据,可以继续展开,展开后我们就可以看到toString方法了。而这个[[prototype]]属性指向的对象就是我们说的原型对象。
我们想访问一个对象的原型对象可以用两种方式进行访问:
var obj = {
name: "aaa",
};
console.log(Object.getPrototypeOf(obj))
console.log(obj.__proto__)
注意,虽然上面两种方式都能访问到对象的原型对象,但是它们是有区别的。__proto__属性是几乎所有浏览器提供的访问属性,而访问对象原型的标准方法是应该是通过Object.getPrototypeOf()这个方法来访问的。
所以从上面我们可以看出,我们自己定义了一个对象,而这个对象中却含有我们没定义过的方法和属性可以供我们使用,而这些方法和属性可以在这个对象的原型对象上找到,所以我们可以认为,我们定义的对象继承了原型对象的属性和方法,这样我们才能通过我们定义的对象来使用这些属性和方法。当我们通过对象的属性来获取value的时候,比如obj.age,它是如何去查找这个属性呢?这就需要引出一个概念,原型链了。
原型链
前面解释过,原型链所指的就是将对象与对象自身的原型对象、原型对象的原型对象串联起来的一个链条,所以当你试图访问一个对象的属性时它就会沿着这个对象的原型链来查找:如果在对象本身中找不到该属性,就会在原型中搜索该属性。如果仍然找不到该属性,那么就搜索原型的原型,以此类推,直到找到该属性,或者到达链的末端,在这种情况下,返回 undefined。这里说明一下,当我们通过一个对象不断的去寻找它原型的末端,最终我们找到的就是一个null,当找到null的时候,就会停止对这个属性的查找并返回一个undefined。
再次深度认识原型、原型链、原型对象
看了上面的介绍在这里可以大致理清楚原型、原型对象、原型链是个什么东西了,为什么说这三个概念很重要呢,这就要涉及到js的面向对象了。我们知道,面向对象有三大特性:封装、继承、多态。而其中的继承的实现,就与我们讲的原型和原型链密不可分。
构造函数
先看下面代码:
function Person(name, age) {
this.name = name;
this.age = age;
}
var p = new Person("kobe", 12);
实际上,当我们new一个对象的时候,构造函数进行了以下操作:
- 在内存中创建一个空对象
- 将该对象的[[prototype]]指向构造函数的prototype属性
- 构造函数里的this指向这个空对象
- 开始执行构造函数的函数体代码
- 如果构造函数未返回内容,将该对象作为返回值返回出去
从上面的步骤2中我们可以看到,在创建对象都时候,会将函数的prototype属性赋值给对象的[[prototype]],这两个对象的含义是不一样的,不是同一个东西。
函数在被创建的时候会为这个函数增加一个属性prototype,而prototype指向一个对象,这个对象含有一个对象,对象里面又有一个属性constructor,而这个属性指向的是构造函数本身,也就是说Person.prototype.constructor === Person
实际上,我们在进行字面量创建对象的时候,也等价于通过new Object的方法进行创建对象的,只不过字面量创建对象更方便了。如下代码:
var obj = {
name:"kobe"
}
//等价于
var obj1 = new Object({name:"kobe"})
console.log(obj.__poto__ === obj1.__proto__,obj1.__proto__ === Object.prototype)//true true
实际上构造函数在js中也能当作普通函数直接使用,比如Person()来进行调用,只是对于在大多数编程语言中,大家慢慢的形成了一种规范,将第一个字母进行大写的函数认为是一个构造函数。而这个构造函数我们也可以说它是一个类,比如上面Person函数,我们也可以说它是一个Person类,它创建出来的对象,就是Person类型的对象。
通过原型链实现继承
上面解释了一个对象通过new关键字调用构造函数创建一个对象的过程,那么在JS中如何体现继承呢?
function Person(name,age){
this.name = name;
this.age = age;
}
function Student(sno){
this.sno = sno
}
var stu = new Student("123")
看上列代码,通过上面说明我们可以知道,如果我们直接打印stu.age肯定得到的是undefined,因为无论是stu对象本身还是通过对它原型链进行查找都找不到age属性的。这时,我们如果将stu的[[protodtype]]属性指向Person类创建的对象,那么不就可以通过原型链获取age属性了吗?
function Person(name,age){
this.name = name;
this.age = age;
}
function Person(name,age,friends){
this.name = name;
this.age = age;
this.friends = friends
}
function Student(sno){
this.sno = sno
}
var stu = new Student("123")
var p = new Person("kobe",12,['a','b','c'])
stu.__proto__ = p
console.log(stu.age)
但先别急,我们总不能每次想让stu有age属性都要自己去创建一个p对象再将stu的原型对象替换成p对象吧。我们前面还知道,对象在被构建函数创建的时候,构建函数的prototype属性会赋值给对象的[[prototype]]属性,那我们直接将构造函数的prototype指向Person创建的对象不就一劳永逸了吗?不用每次创建对象都要进行替换了。
function Person(name,age,friends){
this.name = name;
this.age = age;
this.friends = friends
}
function Student(sno){
this.sno = sno
}
var p = new Person("kobe",12,['a','b','c'])
Student.prototype = p
var stu = new Student("123")
console.log(stu.age)//12
但其实这样做还是会产生弊端:
- 第一,我们通过直接打印对象是看不到这个属性的,因为这个属性在它的原型对象上;
- 第二,这个属性如果是引用类型会被多个对象共享,比如上面代码若
var stu2 = new Student("111"),修改stu2.friends = ['111'],那么stu.friends也会被修改为['111'] - 第三,不能给Person传递参数,因为这个对象是一次性创建的(没办法定制化);
当然肯定是有解决方法的,上面所讲也是为了让大家明白原型链继承的思路是什么。现在针对上面三个问题我们进行解决:
- 如果想打印的时候让继承的属性显示出来,那么这个属性就需要在这个对象本身上面,那么我们可以通过在Student内去调用Person.call(this)即可,因为这里传入的this就指向了创建的对象上,而调用Person因为Person的this已经被绑定在了这个对象上所以在Person里调用this.name赋值实际上就是为我们Student创建的对象进行赋值
- 上述方案也能解决引用类型的属性继承导致的共享问题,因为每次创建新的对象都会重新为该对象创建新的属性。
- 同1解决方案一样,通过call方法将Students接收的参数传递给Person
除了上面的弊端以外,我们还需要考虑,在js的继承中不仅是对对象属性的继承,还有方法的继承,现在给出比较好的解决继承代码,我们在代码中作解释,代码和注释如下
function Person(name, age, friends) {
this.name = name;
this.age = age;
this.friends = friends;
}
//在构造函数上添加方法,这样添加方法就不需要我们每次新增一个对象的时候,都需要再重新创建一个方法再为这个对象赋值,节省内存,但需要注意的是这个方法是所有通过Person1类创建的对象的共享方法
Person.prototype.running = function () {
console.log("running~");
};
function Student(name, age, friends, sno) {
Person.call(this, name, age, friends);
this.sno = sno;
}
//这里我们重新创建了一个对象,并将该对象的原型[[prototype]]指向Person的prototype,然后我们把Student的prototype属性指向了这个新建的对象
Student.prototype = Object.create(Person.prototype);
//因为上述操作我们将Student的prototype指向了我们新建的一个对象,但是该对象中并没有constructor属性,所以我们需要手动创建这个属性并将这个属性指向Student,这样我们在打印对象的时候才会打印出这个对象正确的所属类
Object.defineProperty(Student.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Student,
});
Student.prototype.studying = function () {
console.log("studying~");
};
var stu = new Student("why", 18, ["kobe"], 111);
console.log(stu);
stu.studying();
stu.running();
扩展与提升
下面一张图就是考验你对于原型和原型链是否掌握。注意以下几点:
- 所有的对象在原型链上最终都会指向Object这个构造函数的prototype,可以理解成是个对象在原型链上肯定有的[[prototype]]属性都会指向Object的prototype
- 在js中函数也是个对象,所以Object这个构造函数也会有自己的[[prototype]]属性,但是这个属性会是null
- 所有的函数都是由new Function创建出来的,所以在所有函数的原型链上,肯定有[[prototype]]指向Function的prototype
- Function.prototype是一个对象,所以它自己肯定也是由new Object创建出来的,所以它的[[prototype]]肯定会指向Object.prototype
- Object这个构造函数本身也是个函数,是通过new Function出来的所以它的[[prototype]]肯定也指向Function.prototype
- Function是特殊的,它的[[prototype]]和prototype都指向Function.prototype
es6中的class以及class通过babel转换为es5的源码
其实es6中的通过class定义的类和class类的继承大致原理就是上述代码,只不过它还考虑了一些别的边界情况,还有一些静态方法等的继承实现,class就是一个实现上述代码的语法糖,这里给出通过babeljs转换得到的代码,感兴趣的可以研究一下:
//babel转换前
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
eating() {
console.log(this.name + " eating~")
}
}
class Student extends Person {
constructor(name, age, sno) {
super(name, age)
this.sno = sno
}
studying() {
console.log(this.name + " studying~")
}
}
//babel转换后
//继承函数,传入子类和父类
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true },
});
Object.defineProperty(subClass, "prototype", { writable: false });
if (superClass) _setPrototypeOf(subClass, superClass);
}
//继承父类的静态函数
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf
? Object.setPrototypeOf.bind()
: function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
//es6的super函数实现,创建super函数,将子类接收到的参数传给父类并执行
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
} else if (call !== void 0) {
throw new TypeError(
"Derived constructors may only return object or undefined"
);
}
return _assertThisInitialized(self);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
);
}
return self;
}
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(
Reflect.construct(Boolean, [], function () {})
);
return true;
} catch (e) {
return false;
}
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf.bind()
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _typeof(obj) {
"@babel/helpers - typeof";
return (
(_typeof =
"function" == typeof Symbol && "symbol" == typeof Symbol.iterator
? function (obj) {
return typeof obj;
}
: function (obj) {
return obj &&
"function" == typeof Symbol &&
obj.constructor === Symbol &&
obj !== Symbol.prototype
? "symbol"
: typeof obj;
}),
_typeof(obj)
);
}
//传入校验,检查传入的是否是构造函数(类)
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
}
}
//创建类的时候,通过传入的参数不同,实现将函数和静态函数写到类本身上和类的prototype上
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", { writable: false });
return Constructor;
}
function _toPropertyKey(arg) {
var key = _toPrimitive(arg, "string");
return _typeof(key) === "symbol" ? key : String(key);
}
function _toPrimitive(input, hint) {
if (_typeof(input) !== "object" || input === null) return input;
var prim = input[Symbol.toPrimitive];
if (prim !== undefined) {
var res = prim.call(input, hint || "default");
if (_typeof(res) !== "object") return res;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return (hint === "string" ? String : Number)(input);
}
//创建person类
var Person = /*#__PURE__*/ (function () {
function Person(name, age) {
_classCallCheck(this, Person);
this.name = name;
this.age = age;
}
_createClass(Person, [
{
key: "eating",
value: function eating() {
console.log(this.name + " eating~");
},
},
]);
return Person;
})();
//创建Student类并继承Person
var Student = /*#__PURE__*/ (function (_Person) {
_inherits(Student, _Person);
var _super = _createSuper(Student);
function Student(name, age, sno) {
var _this;
_classCallCheck(this, Student);
_this = _super.call(this, name, age);
_this.sno = sno;
return _this;
}
_createClass(Student, [
{
key: "studying",
value: function studying() {
console.log(this.name + " studying~");
},
},
]);
return Student;
})(Person);