原文发布在dreamapplehappy/blog,本文如如有更新,都会在个人博客进行更新。javascript
咱们最想夸耀的事物,就是咱们所未拥有的事物 《罗生门》- 芥川龙之介java
JavaScript的原型与继承是每个学习JavaScript的同窗都会面对的一个问题,也是不少面试的必考题目; 可是常常会有一些同窗对此只知其一;不知其二,或者是浅尝辄止;这是由于不少讲解原型与继承的文章写的不是那么通俗易懂, 而本文的目的就是一次性的帮助你们把这一系列的知识点梳理清楚;但愿我此次可以作一个好的投球手。git
首先咱们须要知道的是,JavaScript是一种动态语言,本质上说它是没有Class
(类)的;可是它也须要一种继承的方式, 那就是原型继承;JavaScript对象的一些属性和方法都是继承自别的对象。github
不少同窗对JavaScript的原型和继承不是很理解,一个重要的缘由就是你们没有理解__proto__
和prototype
这两个属性的意思。 接下来咱们先来好好梳理一下这两个属性,看看它们存在哪里,表明了什么意义,又有什么做用。面试
首先来讲一下__proto__
这个属性吧,咱们须要知道的是,除了null
和undefined
,JavaScript中的全部数据类型都有这个属性; 它表示的意义是:当咱们访问一个对象的某个属性的时候,若是这个对象自身不存在这个属性, 那么就从这个对象的__proto__
(为了方便下面描述,这里暂且把这个属性称做p0
)属性上面 继续查找这个属性,若是p0
上面还存在__proto__
(p1)属性的话,那么就会继续在p1
上面查找响应的属性, 直到查找到这个属性,或者没有__proto__
属性为止。 咱们能够用下面这两幅图来表示:浏览器
上面这幅图表示在obj
的原型链
上面找到了属性名字是a
的值app
上面这幅图表示在obj
的原型链
上面没有找到属性名字是a
的值函数
咱们把一个对象的__proto__
属性所指向的对象,叫作这个对象的原型
;咱们能够修改一个对象的原型
来让这个对象拥有某种属性,或者某个方法。性能
// 修改一个Number类型的值的原型
const num = 1;
num.__proto__.name = "My name is 1";
console.log(num.name); // My name is 1
// 修改一个对象的原型
const obj = {};
obj.__proto__.name = "dreamapple";
console.log(obj.name); // dreamapple
复制代码
这里须要特别注意的是,__proto__
这个属性虽然被大多数的浏览器支持,可是其实它仅在ECMAScript 2015 规范
中被准确的定义, 目的是为了给这个传统的功能定制一个标准,以确保浏览器之间的兼容性。经过使用__proto__
属性来修改一个对象的原型是很是慢且影响性能的一种操做。 因此,如今若是咱们想要获取一个对象的原型,推荐使用Object.getPrototypeOf
或者Reflect.getPrototypeOf
,设置一个对象的原型推荐使用Object.setPrototypeOf
或者是Reflect.setPrototypeOf
。学习
到这里为止,咱们来对__proto__
属性作一个总结:
null
和undefined
全部其余的JavaScript对象或者原始类型都有这个属性说完__proto__
属性,接下来咱们就要好好的来理解一下prototype
属性了;首先咱们须要记住的是,这个属性通常只存在于函数对象上面; 只要是可以做为构造器的函数,他们都包含这个属性。也就是说,只要这个函数可以经过使用new
操做符来生成一个新的对象, 那么这个函数确定具备prototype
属性。由于咱们自定义的函数均可以经过new
操做符生成一个对象,因此咱们自定义的函数都有prototype
这个属性。
// 函数字面量
console.log((function(){}).prototype); // {constructor: ƒ}
// Date构造器
console.log(Date.prototype); // {constructor: ƒ, toString: ƒ, toDateString: ƒ, toTimeString: ƒ, toISOString: ƒ, …}
// Math.abs 不是构造器,不能经过new操做符生成一个新的对象,因此不含有prototype属性
console.log(Math.abs.prototype); // undefined
复制代码
那这个prototype
属性有什么做用呢?这个prototype
属性的做用就是:函数经过使用new
操做符生成的一个对象, 这个对象的原型(也就是__proto__
)指向该函数的prototype
属性。 那么一个比较简洁的表示__proto__
和prototype
属性之间关系的等式也就出来了,以下所示:
// 其中F表示一个自定义的函数或者是含有prototype属性的内置函数
new F().__proto__ === F.prototype // true
复制代码
咱们可使用下面这张图来更加形象的表示上面这种关系:
看到上面等式,我想你们对于__proto__
和prototype
之间关系的理解应该会更深一层了。
好,接下来咱们对prototype
属性也作一个总结:
new
操做符生成一个对象的内置函数new
操做符生成的对象的原型new
操做符生成的许多对象共享一些方法和属性其实到这里为止,关于JavaScript的原型和继承已经讲得差很少了;下面的内容是一些基于上面的一些拓展, 可让你更好地理解咱们上面所说的。
当咱们理解了上面的知识点以后,咱们就能够对下面的表达式作一个判断了:
// 由于Object是一个函数,函数的构造器都是Function
Object.__proto__ === Function.prototype // true
// 经过函数字面量定义的函数的__proto__属性都指向Function.prototype
(function(){}).__proto__ === Function.prototype // true
// 经过对象字面量定义的对象的__proto__属性都是指向Object.prototype
({}).__proto__ === Object.prototype // true
// Object函数的原型的__proto__属性指向null
Object.prototype.__proto__ === null // true
// 由于Function自己也是一个函数,因此Function函数的__proto__属性指向它自身的prototype
Function.__proto__ === Function.prototype // true
// 由于Function的prototype是一个对象,因此Function.prototype的__proto__属性指向Object.prototype
Function.prototype.__proto__ === Object.prototype // true
复制代码
若是你可以把上面的表达式都梳理清楚的话,那么说明你对这部分知识掌握的仍是不错的。
谈及JavaScript的原型和继承,那么咱们还须要知道另外一个概念;那就是constructor
,那什么是constructor
呢? constructor
表示一个对象的构造函数,除了null
和undefined
之外,JavaScript中的全部数据类型都有这个属性; 咱们能够经过下面的代码来验证一下:
null.constructor // Uncaught TypeError: Cannot read property 'constructor' of null ...
undefined.constructor // Uncaught TypeError: Cannot read property 'constructor' of undefined ...
(true).constructor // ƒ Boolean() { [native code] }
(1).constructor // ƒ Number() { [native code] }
"hello".constructor // ƒ String() { [native code] }
复制代码
咱们还可使用下面的图来更加具体的表现:
可是其实上面这张图的表示并不算准确,由于一个对象的constructor
属性确切地说并非存在这个对象上面的; 而是存在这个对象的原型上面的(若是是多级继承须要手动修改原型的constructor
属性,见文章末尾的代码),咱们可使用下面的代码来解释一下:
const F = function() {};
// 当咱们定义一个函数的时候,这个函数的prototype属性上面的constructor属性指向本身自己
F.prototype.constructor === F; // true
复制代码
下面的图片形象的展现了上面的代码所表示的内容:
关于constructor
还有一些须要注意的问题,对与JavaScript的原始类型来讲,它们的constructor
属性是只读的,不能够修改。 咱们能够经过下面的代码来验证一下:
(1).constructor = "something";
console.log((1).constructor); // 输出 ƒ Number() { [native code] }
复制代码
固然,若是你真的想更改这些原始类型的constructor
属性的话,也不是不能够,你能够经过下面的方式来进行修改:
Number.prototype.constructor = "number constructor";
(1).constructor = 1;
console.log((1).constructor); // 输出 number constructor
复制代码
固然上面的方式咱们是不推荐你在真实的开发中去使用的,若是你想要了解更多关于constructor
的内容,能够看看Object.prototype.constructor 。
接下来,我会使用一些代码来把今天讲解的知识再大体的回顾一下:
function Animal(name) {
this.name = name;
}
Animal.prototype.setName = function(name) {
this.name = name;
};
Animal.prototype.getName = function(name) {
return this.name;
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
// 由于上面的语句将咱们原来的prototype的指向修改了,因此咱们要从新定义Dog的prototype属性的constructor属性
Reflect.defineProperty(Dog.prototype, "constructor", {
value: Dog,
enumerable: false, // 不可枚举
writable: true
});
const animal = new Animal("potato");
console.log(animal.__proto__ === Animal.prototype); // true
console.log(animal.constructor === Animal); // true
console.log(animal.name); // potato
const dog = new Dog("potato", "labrador");
console.log(dog.name); // potato
console.log(dog.breed); // labrador
console.log(dog.__proto__ === Dog.prototype); // true
console.log(dog.constructor === Dog); // true
复制代码
这篇文章到这里基本上能够告一段落了,可是其实关于JavaScript的原型与继承还有许多内容,也还有许多能够研究的地方;可是这篇文章到这里就算是结束了。
我后面还会写一些关于JavaScript原型与继承的内容,若是你们有兴趣的话,能够关注一下;也能够点击这里发表留言。