您的位置:首页 > Web前端 > JavaScript

JavaScript 函数原型链解析

2017-12-09 19:33 399 查看
JavaScript
中,函数原型链是最强大也是最容易让人迷惑的特性。长期以来对于
prototype
__proto__
的一知半解导致在实际开发中经常遇到难以排查的问题,所以有必要将
JavaScript
中的原型概念理解清楚。

1.
__proto__
vs
prototype

1.1
__proto__

JavaScript
中所有对象都拥有一个
__proto__
用来表示其原型继承,所谓的原型链也就是根据
__proto__
一层层向上追溯。
JavaScript
中有一个内置属性
[[prototype]]
(注意不是
prototype
)来表征其原型对象,大多数浏览器支持通过
__proto__
来对齐进行访问。一个普通对象的
__proto__
Object.prototype
:

var a = {
'h' : 1
}

// output: true
a.__proto__ === Object.prototype


1.2
prototype

prototype
是只有函数才有的属性。

当创建函数时,
JavaScript
会自动给函数创建一个
prototype
属性,并指向原型对象
functionname.prototype


JavaScript
可以通过
prototype
__proto__
在两个对象之间建立一个原型关系,实现方法和属性的共享,从而实现继承。

1.3 构造函数创建对象实例

JavaScript
中的函数对象有两个不同的内部方法:
[[Call]]
Construct


如果不通过
new
关键字来调用函数(比如call,apply等),则执行
[[Call]]
方法,该种方式只是单纯地执行函数体,并不创建函数对象。

如果通过
new
关键字来调用函数,执行的是
[[Constrcut]]
方法,该方法会创建一个实例对象,同时将该对象的
__proto__
属性执行构造函数的
prototype
也即
functionname.prototype
,从而继承该构造函数下的所有实例和方法。

有了以上概念后,来看一个例子:

function Foo(firstName, lastName){
this.firstName = firstName;
this.lastName = lastName;
}
Foo.prototype.logName = function(){
Foo.combineName();
console.log(this.fullName);
}
Foo.prototype.combineName = function(){
this.fullName = `${this.firstName} ${this.lastName}`
}

var foo = new Foo('Sanfeng', 'Zhang');
foo.combineName();
console.log(foo.fullName); // Sanfeng Zhang
foo.logName(); // Uncaught TypeError: Foo.combineName is not a function


明明声明了
Foo.prototype.logName
,但是
Foo.combineName
却出错,其原因在于原型链理解出错。

首先来看下
foo
的原型链:

var foo = new Foo('Sanfeng', 'Zhang')
:

通过
new
创建一个函数对象,此时
JavaScript
会给创建出来对象的
__proto__
赋值为
functionname.protoye
也即
Foo.prototype
,所以
foo.combineName
可以正常访问
combineName
。其完整原型链为:

foo.__proto__ === Foo.prototype
foo.__proto__.__proto__ === Foo.prototype.__proto__ === Object.prototype
foo.__proto__.__proto__.__proto__ === Foo.prototype.__proto__.__proto__ === Object.prototype.__proto__ === null




接下来看下Foo的原型链:

直接通过
Foo.combineName
调用时,
JavaScript
会从
Foo.__proto__
找起,而
Foo.__proto__
指向
Function.prototype
,所以根本无法找到挂载在
Foo.prototype
上的
combineName
方法。

其完整原型链为:

Foo.__proto__ = Function.prototype;
Foo.__proto__.__proto__ = Function.prototype.__proto__;
Foo.__proto__.__proto__.__proto__ = Function.prototype.__proto__.__proto__ = Object.prototype.__proto__ = null;




接下来做一下变形:

function Foo(firstName, lastName){
this.firstName = firstName;
this.lastName = lastName;
}

Foo.__proto__.combineName = function() {
console.log('combine name');
}

Foo.combineName(); // combine name
Funciton.combineName(); // combine name
var foo = new Foo('Sanfeng', 'Zhang');
foo.combineName(); // foo.combineName is not a function


这次是在
Foo.__proto__
上注册的
combineName
,所以实例对象foo无法访问到,但是
Function Foo
可以访问到,另外我们看到因为
Foo.__proto__
指向
Function.prototype
,所以可以直接通过
Function.combineName
访问。

2 原型继承

理解清楚了
__proto__
prototype
的联系和区别后,我们来看下如何利用两者实现原型继承。首先来看一个例子:

function Student(props) {
this.name = props.name || 'unamed';
}

Student.prototype.hello = function () {
console.log('Hello ' + this.name);
}

var xiaoming = new Student({name: 'xiaoming'}); // Hello xiaoming


这个很好理解:

xiaoming -> Student.prototype -> Object.prototype -> null


接下来,我们来创建一个
PrimaryStudent
:

function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || 1;
}


其中
Student.call(this, props);
仅仅执行Student方法,不创建对象,参考1.3节中的
[[Call]]


此时的原型链为:

new PrimaryStudent() -> PrimaryStudent.prototype -> Object.prototype -> null


可以看到,目前
PrimaryStudent
Student
并没有任何关联,仅仅是借助
Student.call(this, props);
声明了
name
属性。

要想继承
Student
必须要实现如下的原型链:

new PrimaryStudent() -> PrimaryStudent.prototype -> Student.prototype -> Object.prototype -> null


当然可以直接进行如下赋值:

PrimaryStudent.prototype = Student.prototype


但这样其实没有任何意义,如此一来,所以在
PrimaryStudent
上挂载的方法都是直接挂载到
Student
的原型上了,
PrimaryStudent
就显得可有可无了。

那如何才能将方法挂载到
PrimaryStudent
而不是
Student
上呢?其实很简单,在
PrimaryStudent
Student
之间插入一个新的对象作为两者之间的桥梁:

function F() {}
F.prototype = Student.prototype;
PrimaryStudent.prototype = new F();
PrimaryStudent.prototype.constructor = PrimaryStudent;

// 此时就相当于在new F()对象上添加方法
PrimaryStudent.prototype.getGrade = function() {

}


如此一来就实现了PrimaryStudent与Student的继承:

new PrimaryStudent() -> new PrimaryStudent().__proto__ -> PrimaryStudent.prototype -> new F() -> new F().__proto__ -> F.prototype -> Student.prototype -> Object.prototype -> null


3 关键字
new

实际开发中,我们总是通过一个
new
来创建对象。那么为什么
new
可以创建一个我们需要的对象?其与普通的函数执行有什么不同呢?

来看下下面这段代码:

function fun() {
console.log('fun');
}
fun();
var f = new fun();


其对应的输出都是一样的:

fun
fun


但实际上,两者有着本质的区别,前者是普通的函数执行,也即在当前活跃对象执行环境内直接执行函数fun。

new fun()
的实质却是创建了一个
fun
对象,其含义等同于下文代码:

function new(constructor) {
var obj = {}
Object.setPrototypeOf(obj, constructor.prototype);
return constructor.apply(obj, [...arguments].slice(1)) || obj
}


可以看到,当我们执行
new fun()
时,实际执行了如下操作:

创建了一个新的对象。

新对象的原型继承自构造函数的原型。

以新对象的 this 执行构造函数。

返回新的对象。如果构造函数返回了一个对象,那么这个对象会取代整个 new 出来的结果

从中也可以看到,其实new关键字也利用了原型继承来实现对象创建。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  javascript