原型
背景知识
在基于类的传统面向对象的编程语言中,对象由类实例化而来,实例化的过程中,类的属性和方法会拷贝到这个对象中;对象的继承实际上是类的继承,在定义子类继承于父类时,子类会将父类的属性和方法拷贝到自身当中。因此,这类语言中,对象创建和继承行为都是通过拷贝完成的。
但在JavaScript,它是没有类
的概念的!没有类!没有类!
对象的“继承”并是不存在拷贝行为的,而是一种对象间的原型引用关系(在小黄书中把其叫做行为委托)
。
ES6 中的class也只不过是语法🍬 ,本质还是建立在 [[prototype]] 机制上,并非真正意义上的类。
想必你们都被它骗了吧?坏得很!
先来认识一下对象和函数
在JavaScript中,对象分为普通对象和函数对象。
我们先分别来看看一个普通对象/函数里有什么,在浏览器下执行以下的代码:
let obj = {}
console.dir(obj)
console.log("obj 的类型"+typeof obj);
function Fn() {}
console.dir(Fn)
console.log('Fn 的类型' + typeof Fn);
运行结果如下:
我们可以看到:
- 对于一个普通对象来说,隐含着一个属性,我们可以称之为
原型链[[prototype]]
的一个节点 。对象更深入的认识,我们可以看一下这篇文章,就不在本文进行探讨。
原型链其实没有什么神秘的,他本质就是
对其他对象的引用
。当在原本的对象中找不到属性时,就往引用的其他对象里面找。
所有普通的原型链都会指向内置的
Object.prototype
,所以这个Object.prototype
对象里,包含着JavaScript中许多常用的功能。而这个对象的原型链的尽头则是指向null
。
大多数浏览器支持使用
__proto__
来获取[[prototype]]
,__proto__
的读取器(getter)暴露了一个对象的内部[[Prototype]]
,但该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性,在后续的ES标准中,提供相关的API可以使用。
- 对一个函数来说,它其实也是一个对象,它区别于普通对象来说,最主要的区别是多了一个
prototype
属性,称为原型
,原型的作用就是共享方法。- 里面一个默认的
constructor
属性,用于记录实例是由哪个函数创建; - 当该函数被new一个实例时,实例对象的原型链则指向它,所以我们可能会在函数的prototype中定义所谓公共方法。
- 里面一个默认的
new也是一个语法🍬 ,在这篇文章中,也描述了它的执行过程。
属性设置和屏蔽
在上一篇文章我们提到了[[Put]],我们来讲讲当原对象不存在该属性时,[[Put]]进行的操作:
遍历原型链,如果还是找不到同名属性,那么直接在该对象中添加该属性
如果在原型链上能找到该属性呢,则会进行下面的判断:
如果该属性没有被标记为只读(
writable:false
),那么直接添加到对象本身如果该属性被标记为只读(
writable:false
),那么忽略该语句,不会发生屏蔽。在严格模式下会报错如果该属性是一个setter,那么会调用这个setter,不会发生屏蔽,也不会重新定义。
new一个函数
在JavaScript中,我们把带new的函数调用,称之为构造函数调用
;当我们使用new时,在这过程中会执行[[prototype]]链的链接。我们来试试:
function Bird() {
console.log('Bird');
}
console.dir(Bird)
let bird = new Bird();
console.dir(bird);
我们会发现,原型链上的constructor指向Bird(),那么有人会认为bird由Bird构造,因为constructor指向Bird。
可当我把Bird的prototype修改了,实例化对象会有什么变化
function Bird() {
console.log('Bird');
}
Bird.prototype = {};
let bird = new Bird();
console.dir(bird);
通过这两张图,我们验证了bird实例的原型链指向其“构造函数”的prototype上。
同时,我们也知道prototype上的constructor默认指向该函数,但prototype是可变的,当我们对其进行改变,表面上bird还是Bird构造的,但是bird.constructor并非指向Bird,而是Object。(事实上默认的constructor已经被摧毁,当前指向的是 原型链上一层的constructor)
因此,靠constructor来判断其由谁构造是不可取的! 我们也可以理解到,在JavaScript中并没有构造函数可言,因为new
函数调用就变成一个构造函数调用。
一般来说,是不允许直接改变原型prototype的指向。
在ES6中,提供
Object.setPrototypeOf()
来修改关联。
例题
// a
function Foo () {
getName = function () {
console.log(1);
}
return this;
}
// b
Foo.getName = function () {
console.log(2);
}
// c
Foo.prototype.getName = function () {
console.log(3);
}
// d
var getName = function () {
console.log(4);
}
// e
function getName () {
console.log(5);
}
Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 1
getName(); // 1
new Foo.getName(); // 2
new Foo().getName(); // 3
new new Foo().getName(); // 3
类思想中的继承
在ES6之前,我们可以通过构造函数+原型对象模拟实现。
function Father(name) {
this.name = name;
}
Father.prototype.dance = function () {
return 'dance';
};
function Son(name, age) {
Father.call(this, name);
this.age = age;
}
let son = new Son('merlin', 100);
son.dance(); //报错
如何继承父类的方法呢?
如果使用Son.prototype = Father.prototype
,我们知道,原型是一个对象,用=操作符实际上是引用关系而不是复制。因此如果我们修改子类的原型方法,父类的原型同样会受到影响。
因此我们可以将子类的原型指向父类的实例,会生成一个新对象,并且原型链会指向父类的原型,这样子既可以共享父类的方法,为子类增加方法,父类也不会受影响。
Son.prototype = new Father();
Son.prototype.singAndDance = function () {
console.log(this.name + ' sing and' + this.dance());
};
ES6之后,引入Class语法🍬 ,使代码更具备可读性。
class的引入,只是摒弃了之前那种丑陋的语法,其本质上还是通过[[prototype]]机制实现的。实现还是“类”的思想。
class Father {
constructor(name) {
this.name = name;
}
dance() {
return 'dancing';
}
}
class Son extends Father {
constructor(name, score) {
super(name);
this.score = score;
}
singAndDance() {
console.log(this.name + ' sing and' + this.dance());
}
}
class必须被new调用,否则会报错。这是他与普通构造函数的一个主要区别,后者不需要new也可以执行。
类的所以实例共享一个原型变量。
类的内部,默认是严格模式
更简洁的设计
我们可以基于行为委托机制,使这个过程变得更直接,明了。
let father = {
init(name) {
this.name = name;
},
dance() {
return 'dancing';
},
};
let son = {
singAndDance() {
console.log(this.name + ' sing and' + this.dance());
},
};
Object.setPrototypeOf(son, father);
son.init('son')
son.singAndDance()
说到底,基于类的实现到最后创建出来的也是对象,只不过将过程抽象成父类子类的形式。当使用对象来进行设计代码时,语法更加简明,结构更加清晰。
行为委托认为,对象之间是兄弟关系,互相委托,而不是父类和子类的关系,JavaScript中的
[[prototype]]
本质上就是行为委托机制。对象关联倡导直接创建和关联对象,不把它们抽象成类。对象关联可以基于
[[prototype]]
的行为委托非常自然的实现——《你不知道的JavaScript》上册
原型链梳理
function Person(name) {
this.name = name
}
var p2 = new Person('king');
console.log(p2.__proto__) //Person.prototype
console.log(p2.__proto__.__proto__) //Object.prototype
console.log(p2.__proto__.__proto__.__proto__) // null
console.log(p2.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.__proto__.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
console.log(p2.constructor)//Person
console.log(p2.prototype)//undefined p2是实例,没有prototype属性
console.log(Person.constructor)//Function 一个空函数
console.log(Person.prototype)//打印出Person.prototype这个对象里所有的方法和属性
console.log(Person.prototype.constructor)//Person
console.log(Person.prototype.__proto__)// Object.prototype
console.log(Person.__proto__) //Function.prototype
console.log(Function.prototype.__proto__)//Object.prototype
console.log(Function.__proto__)//Function.prototype
console.log(Object.__proto__)//Function.prototype
console.log(Object.prototype.__proto__)//null