1. 什么是原型,为啥要有原型?
什么是原型,为啥要有原型呢?这问题一上来确实不好回答。不妨换个思路思考:如果JS中没有原型,会怎么样?
首先,如果没有原型,对象的一些方法我们可能无法调用了
比如:
let str = 'hello, world';
str.split(','); // ['hello', ' world']
str.hasOwnProperty('length'); // true
上面的代码我们定义了一个字符串str
,并调用了它split
和hasOwnProperty
方法。你是否想过,为什么str
会有这些方法,它们是在哪里定义的?
我们能调用这两个方法,借助的就是原型的力量。
我们能调用split
方法是因为str
是String
类型的,而String
的原型上就定义了split
方法。
你可使用Object.getOwnPropertyNames
发现它们:
String.prototype
上并没有发现hasOwnProperty
方法,为啥也能调用呢?这是因为String
是Object
的子类,而Object
的原型上定义了hasOwnProperty
方法:
其次,如果没有原型,很多方法可能都要以全局方法的方式存在,就像parseInt
和parseFloat
一样。记忆这些方法是件难事,而原型像是容器,将一个类的方法都聚到一起,使我们可以直接通过对象访问它们。
如果你有面向对象的基础知识的话,你会发现上面说的就是对象三大特性的“封装”和“继承”。
所以什么是原型,为啥要有原型?简单说就是为了实现面向对象
。条条大路通罗马,有些编程语言的面向对象是基于类(class
)的,像Java,有些是基于原型(prototype
)的,像JS。
上面我故意避开原型、原型链的一些知识(后面会讲),就是为了方便大家理解。
2. prototype、proto、[[Prototype]]
JS里面的对象都是有原型的(Object.create(null)
除外),一些书籍、标准或规范喜欢用[[Prototype]]
来表示。
而对象又分为函数对象,以及函数对象new
出来的普通对象。
- prototype 是函数对象的属性,代表函数的原型;
- __proto__是普通对象的属性,用来指向对应函数构造器的原型;
我们用Fn
代表函数对象,用obj
代表它创建的普通对象,那么:
obj.__proto__ === Fn.prototype; // obj 是 Fn创建的
Fn.__proto__ === Function.prototype; // Fn 是Function创建
不管是prototype
,还是__proto__
,它们都是原型对象[[Prototype]]
的引用,或者说访问器。只是prototype
是函数用来访问原型的,而__proto__
是普通对象用来访问原型的。它俩指向的都是同一个原型对象。
![](https://i-blog.csdnimg.cn/blog_migrate/82c2806428eea6fe4d4947f480d9b1e2.png)
3. 用现代方法操作原型
__proto__
并不推荐使用,一是只有浏览器才支持的比较好,二是__proto__
可能被误当做对象的键而被修改,导致代码产生bug。
我们应该使用更现代的方法代替__proto__
,比如:
Object.create(proto, [descriptors])
—— 利用给定的原型和可选的属性描述来创建一个新对象。Object.getPrototypeOf(obj)
—— 返回指定对象的原型。Object.setPrototypeOf(obj, proto)
—— 设置对象的原型。
使用Object.setPrototypeOf
方法要求传入的原型要么是null要么是一个对象。这里的“对象”指的是引用类型的对象,如果是非null的基本类型就会报错:
但是直接使用__proto__
修改原型就能绕过限制:
所以使用__proto__
并不是很安全,但是为了方便演示,下面内容可能还会出现它的身影。
4. 原型链
前面说了对象都有__proto__
属性,而在JS中万物皆对象,obj.__proto__
也是个对象,它也有__proto__
属性,也就是obj.__proto__.__proto__
,只要__proto__
存在,就可以这么一直访问下去,直到__proto__
返回null
。这样就形成了一个链条,叫做原型链。
![](https://i-blog.csdnimg.cn/blog_migrate/7be706fd18ce0066951143bfa993df92.png)
上面是原型链的简单示意图,可以看到:
- 所有对象的原型最终都会追溯到
Object.prototype
,所以对象都是Object
的实例 - 所有函数对象都是
Function
的实例,Object
也是函数,所以Function
和Object
互为父亲. Object.prototype.__proto__
值为null
,因为原型链不能形成闭环。如果Object.prototype.__proto__
为某个非空对象,那这个对象的原型链上最终还会出现Object.prototype
,就形成了一个环,原型链就无法结束了。
![](https://i-blog.csdnimg.cn/blog_migrate/a4d3e914d8d2cf5ebe2c32faf41ca16e.png)
为了避免成环,JS不允许用户向Object.prototype.__proto__
赋值:
自定义的函数也不能让原型链成环:
那原型链有什么用途呢,我理解主要有两点:
- 属性继承,当访问对象的属性时,首先从对象自身查找,没有就沿着原型链查找,直到找到或者原型链结束为止;
- instanceof,instanceof的原理就是借助原型链;
instanceof在我的另一篇文章《js typeof、instanceof区别一次给你讲明白》中有详细介绍,感兴趣的可以去看,这里重点介绍原型和继承。
5. 原型继承
JS采用的是原型继承机制,这个和Java等编程语言的类继承机制不同,原型继承机制是通过原型对象继承,而类继承机制则是通过类继承。
不要带着对类继承的认识,看待JS的原型继承,这样你会很别扭。
JS的继承可以发生在两个普通对象之间,比如:
let animal= { name: 'animal' };
let dog = { __proto__: animal };
dog.name; // animal
我们可以说:对象dog
从原型对象animal
上继承属性。
不过为了大家理解方便,我下面还是用Animal
和Dog
这两个类,来演示原型继承。
直接使用原型链实现继承:
function Animal() { }; // 定义一个Animal类
Animal.prototype.run = function () { console.log('奔跑...'); } // 定义父类方法
function Dog() {} // 定义一个Dog类
// 让Dog继承Animal
Dog.prototype.__proto__ = Animal.prototype; // 利用原型链实现继承
let dog = new Dog(); // 定义子类实例
// 调用父类方法
dog.run(); // 奔跑...
继承关系如图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/d59406208bc1af20acf103fe44539e8b.png)
这是学完原型链能直接想到的方式,它还有一种变体:
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复constructor的指向问题
这两种方式本质都是利用原型链,只是由于第二种方式是直接覆盖子类原型,需要注意修复constructor
指向问题,而且给子类原型添加属性和方法时,需要在Object.create
调用之后,否则刚添加的属性和方法又被覆盖了。
直接使用原型链的继承都存在一点问题:只能从原型对象上继承,对于那些定义在父类构造函数中的属性和方法,是无法继承的。。
function Animal() {
this.eat = function() { console.log('疯狂进食...'); } // 实例方法
}; // 定义一个Animal类
Animal.prototype.run = function () { console.log('奔跑...'); } // 原型方法
function Dog() { } // 定义一个Dog类
// 让Dog继承Animal
Dog.prototype.__proto__ = Animal.prototype; // 利用原型链实现继承
let dog = new Dog(); // 定义子类实例
// 调用父类方法
dog.run(); // 奔跑...
dog.eat(); // Uncaught TypeError: dog.eat is not a function
这里的eat
方法是直接在父类构造方法中定义的,Animal.prototype
中并不存在此方法,因此不能被基于原型的继承方式继承。我们可以通过“借用父类构造方法”的方式修复这个问题:
function Animal() {
this.eat = function() { console.log('疯狂进食...'); } // 实例方法
}; // 定义一个Animal类
Animal.prototype.run = function () { console.log('奔跑...'); } // 原型上定义的方法
function Dog() { // 定义一个Dog类
Animal.apply(this, arguments); // 父类构造函数初始化子类的实例,这样父类的实例属性就会赋值给this
}
// 让Dog继承Animal
Dog.prototype.__proto__ = Animal.prototype; // 利用原型链实现继承
let dog = new Dog(); // 定义子类实例
// 调用父类方法
dog.run(); // 奔跑...
dog.eat(); // 疯狂进食...
Animal.apply(this, arguments)
(或者Animal.call
)的作用就是调用父类的构造方法来初始化子类的实例,这样在父类构造方法中定义的属性和方法,就在子类实例上重新赋值了一遍。
不过这里还有一个问题,就是静态属性的继承问题。静态属性和方法就是直接定义在类上的属性和方法,它即不属于原型对象,也不属于实例,而是属于类(或函数)本身,因此无法被继承。
function Animal() {
this.eat = function() { console.log('疯狂进食...'); } // 实例方法
}; // 定义一个Animal类
Animal.KIND = '哺乳动物'; // 静态属性
Animal.printKind = function() { console.log(Animal.KIND); } // 静态方法
其实这也不算是个问题,因为静态属性在一些编程语言中本就无法继承,比如Java。不过JS这门语言太过动态且灵活了,要实现静态属性继承,也是很容易的。
function Animal() {
this.eat = function () { console.log('疯狂进食...'); } // 实例方法
}; // 定义一个Animal类
Animal.KIND = '哺乳动物'; // 静态属性
Animal.printKind = function () { console.log(Animal.KIND); } // 静态方法
Animal.prototype.run = function () { console.log('奔跑...'); } // 原型上定义的方法
function Dog() { // 定义一个Dog类
Animal.call(this); // 借用构造方法,解决实例属性的继承问题
}
// 让Dog继承Animal
Dog.prototype.__proto__ = Animal.prototype; // 利用原型链实现继承
// 给Dog类添加方法
Dog.prototype.woof = function() { console.log('汪汪汪...'); }
// 静态属性继承
Dog.__proto__ = Animal; // 这里是把函数当成对象看待
let dog = new Dog(); // 定义子类实例
// 调用父类方法
dog.run(); // 奔跑...
dog.eat(); // 疯狂进食...
// 调用子类方法
dog.woof(); // 汪汪汪...
// 调用从父类继承的静态成员
Dog.KIND; // 哺乳动物
Dog.printKind(); // 哺乳动物
关键代码就一行:
Dog.__proto__ = Animal;
我们把Dog
当成一个普通对象,将Dog.__proto__
设置成父类,这样当我们访问Dog
的静态属性和方式时,如果Dog
类上没有,就会到Animal
上找。
上面的写法差不多是不借助ES6语法情况下,写出的比较完美的继承了。
下面是完整的继承关系图:
![](https://i-blog.csdnimg.cn/blog_migrate/e47349cce72ec524e190e600267164d7.png)
我们使用三个技巧分别实现了原型属性、实例属性和静态属性的继承,最后我们再总结一下。
实现原型继承:
// 方式1:使用__proto__
Dog.prototype.__proto__ = Animal.prototype;
// 方式2:使用ES6的Object.setPrototypeOf代替__proto__
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
// 方式3:使用Object.create
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
实现父类实例属性继承:
// 在子类构造方法中执行
Animal.apply(this, arguments);
实现静态属性继承:
Dog.__proto__ = Animal;
// 或使用ES6的方法
Object.setPrototypeOf(Dog, Animal);
原型属性继承和静态属性继承本质都是基于原型链,区别在于原型链是作用于类上,还是作用于原型对象上。
实例属性的继承除了上面说的用apply
或call
方法,还有一种写法:
Dog.prototype = new Animal(); // 用父类示例作为子类原型
Dog.prototype.constructor = Dog; // 修复constructor指向
这种方式使用父类的一个实例作为子类的原型,因为是父类的实例,这个实例自然继承了父类的原型属性,同时又包含实例属性。尽管看着很棒,但是由于无法向父类构造方法传参,因此也是个“鸡肋”的方法,了解即可。
6. ES6的面向对象
单就原型的知识,上面的内容已经足够了。本节是一些扩展,不感兴趣,可以不用了解。
我们都知道ES6引入了基于class
的面向对象,你可以用更标准的方式来编写面向对象代码。前面的例子,可以用class
改写一下:
class Animal { // 父类
constructor() {
this.eat = function() { console.log('疯狂进食...'); } // 实例方法
}
run() { console.log('奔跑...'); }
}
class Dog extends Animal { // 子类继承父类
woof() { console.log('汪汪汪...') }
}
let dog = new Dog(); // 定义子类实例
// 调用父类方法
dog.run(); // 奔跑...
// 调用父类的实例方法
dog.eat(); // 疯狂进食...
dog.woof(); // 汪汪汪...
看着简洁很多。
当然本节的内容不是介绍ES6的class
,而是告诉你,ES6的class
只是语法糖,其本质还是原型。上面的代码可以使用Babel将其编译成ES6之前的写法:
"use strict";
function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; _setPrototypeOf(subClass, superClass); }
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var Animal = /*#__PURE__*/function () {
// 父类
function Animal() {
this.eat = function () {
console.log('疯狂进食...');
}; // 实例方法
}
var _proto = Animal.prototype;
_proto.run = function run() {
console.log('奔跑...');
};
return Animal;
}();
// 静态属性
_defineProperty(Animal, "KIND", '哺乳动物');
// 静态方法
_defineProperty(Animal, "printKind", function () {
console.log(Animal.KIND);
});
var Dog = /*#__PURE__*/function (_Animal) {
_inheritsLoose(Dog, _Animal);
function Dog() {
return _Animal.apply(this, arguments) || this;
}
var _proto2 = Dog.prototype;
// 子类继承父类
_proto2.woof = function woof() {
console.log('汪汪汪...');
};
return Dog;
}(Animal);
代码虽多,但仔细看就会发现,使用的技巧都是前文讲过的,如果有不懂的,再反复看看第5小结的内容。
7. 结语
最后,码字不已,还望支持,如果有讲的不对的地方也希望留言更正。