原型和原型链
在js中,所有的变量都有原型,原型也可以有原型,原型最终都指向Object
什么是原型
在js中,一个变量被创建出来,它就会被绑定一个原型;比如说,任何一个变量都可以使用console.log打印,这里是调用了它的toString方法,而变量被创建后可能并没有设置toString方法,但是它任然可以打印,这就从原型中获取的toString方法
所以可以得到第一点:原型可以提供方法给实例的变量,
原型也是一个对象,或者说对象可以作为原型并赋值给其他变量,这样对象成为了变量的原型,而对象本身也有原型,此时就形成了 ‘链’
同样的这个变量也是一个对象,他也可以作为其他变量的原型,这样‘链’就变得更长了,但是所有的链都有一个最终指向(root终点)--- Object,Obejct是最原始的对象,它包含了所有js变量都共享的方法,它不再有原型属性[[prototype]], 所以在js中一切皆对象中的对象就是指的继承自Object
这里可以总结一下:
- 原型是一个对象,它可以向继承了自身的变量提供方法(属性)
- 变量都拥有原型,也可以成为原型,循环下去可以形成‘链’,
- 链的最顶端是Object对象,它提供了最基本的方法
js通过原型来复用通用的方法和属性,这样极大的减少了变量创建的成本(减少了内存支出,原型的方法属性都存储在原型上,变量中不会复制过来占用内存;也不必每个对象都去书写基本的方法),
tips:
原型链:就前面说到的 变量获得原型,原型又有原型,变量又可以作为原型,这样就形成了‘链’,链的最外层(底层),拥有最多的方法,继承了原型链上所有原型的方法和属性
prototype
在 JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]]
(如规范中所命名的),它要么为 null
,要么就是对另一个对象的引用。当我们从 object
中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性。这就是继承,object也可以从中借用方法
prototype不能在变量中被直接引用,通常没有办法遍历或者读取这个属性,所以它在控制台的名称有些特殊,使用[[ ]]引用,但是可以直接去调用它里面的方法,当你使用变量中不存在的属性或者方法,此时会自动向原型中寻找,对应的属性方法并调用,若原型链中没有则返回undefined
但是也有特例,在构造函数中,可以给构造的实例添加原型属性或方法
__proto__
这是一个过时的属性,现在只能在浏览器中使用,它的作用就是指向prototype,给原型添加属性或方法,这样添加的原型属性或方法会被遍历出来(Object.keys,for in),
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // 设置 rabbit.[[Prototype]] = animal
注意,
__proto__
与内部的[[Prototype]]
不一样。__proto__
是[[Prototype]]
的getter/setter;获取和设置原型可以使用函数Object.getPrototypeOf/Object.setPrototypeOf
来替代__proto__
去 get/set 原型
new
当使用 new
关键字调用函数时,该函数将被用作构造函数。new
将执行以下操作:
- 创建一个空的简单 JavaScript 对象作为实例。
- 如果构造函数的
prototype
属性是一个对象,则将实例的 原型[[Prototype]] 指向构造函数的prototype
属性,否则实例将保持为一个普通对象,其 [[Prototype]]为Object.prototype,
因此,通过构造函数创建的所有实例都可以访问添加到构造函数prototype
属性中的属性/对象。- 使用给定参数执行构造函数,并将this指向实例
- 如果构造函数返回非原始值,则该返回值成为整个
new
表达式的结果。否则,如果构造函数未返回任何值或返回了一个原始值,则返回实例。(通常构造函数不返回值,但可以选择返回值,以覆盖正常的对象创建过程。)
new的关键点在于,它新建了一个实例对象,同时引入了原型,改变了构造函数的this指向,让实例对象成为了这个构造函数的构造结果
class
class是es6新增的语法糖,它简化了js中构造类的步骤,隐去了对原型的操作(根本上还是原型的操作),js中的类是基于原型的,使用原型达到继承的效果,
以下使用原型prototype和类class构建一个相同的类,
prototype.js
// 使用原型构造一个P类
// 构造器,大写开头规范(不强制)
function P (x,y){
// this指向实例,此时this不生效,构造实例后指向实例
this.x = x;
this.y = y;
// 实例方法,此处的this指向实例,可以使用实例的属性
this.getXY = ()=>{
return [this.x,this.y]
}
}
// 实例方法,此处的this指向windows,不能使用实例的属性
P.prototype.getPName = ()=>{
return P.name
}
// 实例属性
P.prototype.desc = '2维坐标'
//类方法/静态方法
P.getP = (p1,p2)=>{
return [p1.getXY(),p2.getXY()]
}
// 类属性/静态属性
P.des = '坐标'
class.js
// 使用class构造一个P类
class P{
//构造器
constructor(x,y){
this.x = x;
this.y = y;
}
// 实例属性
desc = '2维坐标'
// 实例方法
getXY(){
return [this.x,this.y]
}
getPName(){
return P.name
}
// 静态属性
static des = '坐标'
// 静态方法
static getP(p1,p2){
return [p1.getXY(),p2.getXY()]
}
}
很明显,class的用法更加整体化,但实际上这两种的写法效果是完成相同的,
可以使用同一串代码来实例化这个类P
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>prototype,class</title>
</head>
<body>
<script src="class.js"></script>
<!-- <script src="prototype.js"></script> -->
<script>
// new关键字,新建一个对象,原型指向类,获得构造器中的this指向
let p = new P(1, 2);
console.log(p);
console.log(p.x);
console.log(p.y);
console.log(p.desc);
console.log(p.getXY());
console.log(p.getPName());
console.log(P.getP(p, p));
console.log(P.des);
</script>
</body>
</html>
实现的效果是一致的方法上略有不同,对于类的构造,关键在于this的指向问题,属性和方法是挂在类名下的,还是挂在实例下的需要区分开,
挂在类下的属性方法被称为,类方法(属性)或者静态方法(属性),通过类名引用(Math.max)
挂在实例下的属性方法被称为,实例方法(属性),通过实例引用(arr.push)
总结
- 在 JavaScript 中,所有的对象都有一个隐藏的
[[Prototype]]
属性,它要么是另一个对象,要么就是null
。 - 通过
[[Prototype]]
引用的对象被称为“原型”。 -
"prototype"
属性仅当设置在一个构造函数上,并通过new
调用时,才具有这种特殊的影响。在常规对象上,prototype
会被当成是一个普通的属性名 - 所以变量都通过原型/原型链使用共享的方法(即自身不存在,从原型/原型链中借用)
- 类的使用要区分实例和静态类,哪些方法属性在类名下,哪些方法属性在实例下