原型与原型链
# 原型与原型链
首先,JS是真正的“面向对象”的语言,而其他我们所熟知的例如C++、Java等,严格意义上说,是“面向类”的语言,仔细想想,还真是那么回事儿。其次,JS中,调用构造器之后,对象并不是它原型的一份拷贝,而是被链接到原型上。
# Object
我们先看一段代码:
function Person(name){
this.name = name;
}
Person.prototype.sayName = function(welcome) {
console.log(welcome, this.name);
}
var person1 = new Person('Smiley');
person1.sayName('Hello');
2
3
4
5
6
7
8
9
10
在执行这段代码之前,有个东西是一直存在的,就是有些人所说的“原型的原型”。如下图所示,我们用圆形代表function,用方形代表object:
左边的圆形是Object构造函数,就是我们一般使用var obj = new Object()时最普普通通的的构造函数。右边的方形是Object的prototype,这个东西没有名字(虽然它很重要)。左边有一个箭头指向右边,意味着Object有一个属性叫做prototype,这个属性指向的是右边的那个方块,而向左指的箭头,意味着右边方块的constructor属性是左边的Object构造函数。
这些东西在上面程序运行之前就一直存在的。
# 构造函数
好的,我们开始看第一行代码,运行第一行代码之后,会生成一个叫Person的构造函数,而这个构造函数的prototype属性,指向的就是它的prototype,如下图方块所示:
Person与Person.prototype之间的关系,与Object和Object.prototype类似,不同的是,Person.prototype会通过__proto__指向Object.prototype。
接下来看第5行代码:我们在Person.prototype上面添加一个sayName方法,如上图所示,Person.prototype这个方块中有sayName方法。
# new调用构造器
使用new这个关键字的时候,JS编译器会做四件事情:
//1.创建了一个全新的对象。
//2.这个对象会被执行[[Prototype]](也就是__proto__)链接。
//3.生成的新对象会绑定到函数调用的this。
//4.如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。
//new运算符的执行过程,实现一个new
function realizeNew(con, ...args) {
let obj = {};
obj.__proto__ = con.prototype;
let ret = con.apply(obj, args);
return typeof ret === 'object' && ret !== null ? ret : obj;
}
//举列
function Person(name,age){
this.name=name; /*属性*/
this.age=age;
this.run=function(){ /*实例方法*/
alert(this.name+'在运动');
}
}
realizeNew(Person,'和振峰',24)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
如上图所示,我们先创建一个person1的空对象,然后把person1通过__proto__指向原型对象,指向构造函数中的代码,person1就获得了一个叫做name的属性,最后返回。
我们最后运行第10行代码:person1.sayName('Hello');
person1上有sayName这个方法么?没有,那么就顺着person1的__proto__向上找,找到Person.prototype。Person.prototype上面有sayName方法么?有的,那么执行这个方法。这个方法内部使用了this.name,那么这个this的指向是什么么?我们需要看sayName的call site,是person1调用的sayName,隐式调用,this就指向person1,而person1的name就是Smiley。
是不是觉得很神奇,最后调用时候使用的属性和方法都是我们希望使用的那个,person1.sayName('Hello')看似很容易理解,JS初学者都能很容易说出最后输出结果,但是这其中的过程,恐怕只有理解了原型和原型链才能真正说明白。
明白了这些之后,我们看几个相等关系:
console.log(Person === Person.prototype.constructor);
console.log(person1.__proto__ === Person.prototype);
2
我们再也不用死记硬背这个关系了,而是通过上面的图直接可以推到出来。
console.log(person1.constructor)是什么的呢?
# es5的几种继承方式
# 对象冒充实现继承
function Person(){
this.name='张三'; /*属性*/
this.age=20;
this.run=function(){ /*实例方法*/
alert(this.name+'在运动');
}
}
Person.prototype.sex="男";
Person.prototype.work=function(){
alert(this.name+'在工作');
}
//Web类 继承Person类 对象冒充的组合继承模式
function Web(){
Person.call(this); /*对象冒充实现继承*/
}
var w=new Web();
// w.run(); //**对象冒充可以继承构造函数里面的属性和方法**
w.work(); //对象冒充可以继承构造函数里面的属性和方法 但是没法继承原型链上面的属性和方法
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 原型链实现继承
function Person(name,age){
this.name='张三'; /*属性*/
this.age=20;
this.run=function(){ /*实例方法*/
alert(this.name+'在运动');
}
}
Person.prototype.sex="男";
Person.prototype.work=function(){
alert(this.name+'在工作');
}
//Web类 继承Person类 原型链+对象冒充的组合继承模式
function Web(name,age){
}
Web.prototype=new Person(); //原型链实现继承
var w=new Web();
//原型链实现继承:可以继承构造函数里面的属性和方法 也可以继承原型链上面的属性和方法
//w.run();
w.work();
//缺点是实例化子类的时候没法给父类传参
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 原型链+对象冒充的组合继承模式
function Person(name,age){
this.name=name; /*属性*/
this.age=age;
this.run=function(){ /*实例方法*/
alert(this.name+'在运动');
}
}
Person.prototype.sex="男";
Person.prototype.work=function(){
alert(this.name+'在工作');
}
function Web(name,age){
Person.call(this,name,age); //对象冒充继承 实例化子类可以给父类传参
}
Web.prototype=new Person();//上面已经继承了构函数的方法 , 这里new 一个会重新继承构造函数的方法 ,所以这里可以直接 -> Web.prototype=Person.prototype;
var w=new Web('赵四',20);
// w.run();
w.work();
// var w1=new Web('王五',22);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# class实现继承源码
ES6
class B {
constructor(props) {
this.name = props.name;
}
}
class A extends B {
constructor() {
// 向父类传参
super({ name: 'B' });
// this 必须在 super() 下面使用
console.log(this);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
ES5
function __extends(child, parent) {
// 修改对象原型
Object.setPrototypeOf(child, parent);
// 寄生继承,创建一个干净的构造函数,用于继承父类的 prototype
// 这样做的好处是,修改子类的 prototype 不会影响父类的 prototype
function __() {
// 修正 constructor 指向子类
this.constructor = child;
}
// 原型继承,继承父类原型属性,但是无法向父类构造函数传参
child.prototype =
parent === null
? Object.create(parent)
: ((__.prototype = parent.prototype), new __());
}
var B = (function () {
function B(props) {
this.name = props.name;
}
return B;
}());
var A = (function (_super) {
__extends(A, _super);
function A() {
var _this = _super.call(this, { name: 'B' }) || this; // // 向父类传参
// this 必须在 super() 下面使用
console.log(_this);
return _this;
}
return A;
}(B));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# ES5/ES6 的继承除了写法以外还有什么区别?
- class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量。
const bar = new Bar(); // it's ok
function Bar() {
this.bar = 42;
}
const foo = new Foo(); // ReferenceError: Foo is not defined
class Foo {
constructor() {
this.foo = 42;
}
}
2
3
4
5
6
7
8
9
10
11
- class 声明内部会启用严格模式。
// 引用一个未声明的变量
function Bar() {
baz = 42; // it's ok
}
const bar = new Bar();
class Foo {
constructor() {
fol = 42; // ReferenceError: fol is not defined
}
}
const foo = new Foo();
2
3
4
5
6
7
8
9
10
11
12
- class 的所有方法(包括静态方法和实例方法)都是不可枚举的。
// 引用一个未声明的变量
function Bar() {
this.bar = 42;
}
Bar.answer = function() {
return 42;
};
Bar.prototype.print = function() {
console.log(this.bar);
};
const barKeys = Object.keys(Bar); // ['answer']
const barProtoKeys = Object.keys(Bar.prototype); // ['print']
class Foo {
constructor() {
this.foo = 42;
}
static answer() {
return 42;
}
print() {
console.log(this.foo);
}
}
const fooKeys = Object.keys(Foo); // []
const fooProtoKeys = Object.keys(Foo.prototype); // []
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。
function Bar() {
this.bar = 42;
}
Bar.prototype.print = function() {
console.log(this.bar);
};
const bar = new Bar();
const barPrint = new bar.print(); // it's ok
class Foo {
constructor() {
this.foo = 42;
}
print() {
console.log(this.foo);
}
}
const foo = new Foo();
const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 必须使用 new 调用 class。
function Bar() {
this.bar = 42;
}
const bar = Bar(); // it's ok
class Foo {
constructor() {
this.foo = 42;
}
}
const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'
2
3
4
5
6
7
8
9
10
11
- class 内部无法重写类名。
function Bar() {
Bar = 'Baz'; // it's ok
this.bar = 42;
}
const bar = new Bar();
// Bar: 'Baz'
// bar: Bar {bar: 42}
class Foo {
constructor() {
this.foo = 42;
Foo = 'Fol'; // TypeError: Assignment to constant variable
}
}
const foo = new Foo();
Foo = 'Fol'; // it's ok
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
← 输入URL背后的技术步骤 leetCode →