JavaScript 函数原型链解析
2017-12-09 19:33
399 查看
在
1.
1.1
在
1.2
当创建函数时,
如果不通过
如果通过
有了以上概念后,来看一个例子:
明明声明了
首先来看下
通过
接下来看下Foo的原型链:
直接通过
其完整原型链为:
接下来做一下变形:
这次是在
这个很好理解:
接下来,我们来创建一个
其中
此时的原型链为:
可以看到,目前
要想继承
当然可以直接进行如下赋值:
但这样其实没有任何意义,如此一来,所以在
那如何才能将方法挂载到
如此一来就实现了PrimaryStudent与Student的继承:
3 关键字
实际开发中,我们总是通过一个
来看下下面这段代码:
其对应的输出都是一样的:
但实际上,两者有着本质的区别,前者是普通的函数执行,也即在当前活跃对象执行环境内直接执行函数fun。
但
可以看到,当我们执行
创建了一个新的对象。
新对象的原型继承自构造函数的原型。
以新对象的 this 执行构造函数。
返回新的对象。如果构造函数返回了一个对象,那么这个对象会取代整个 new 出来的结果
从中也可以看到,其实new关键字也利用了原型继承来实现对象创建。
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(JS)的indexOf和lastIndexOf函数全解析
- 跟我学习javascript的var预解析与函数声明提升
- JavaScript 变量、函数与原型链
- JavaScript自定义日期格式化函数详细解析
- javascript笔记:深入了函数的作用域链及标识符解析的过程
- 使用javascript的eval函数解析json字符串,提示“not avalible”怎么办?
- javascript对象,函数,原型链
- Javascript 日期解析/检查函数
- javascript eval函数解析json数据时为什么 加上圆括号
- JavaScript-函数(二)变量作用域与解析赋值
- javascript eval函数解析json数据时为什加上圆括号eval("("+data+")")
- 从JavaScript的继承角度解析什么是原型链
- javascript eval函数解析json数据时为什加上圆括号eval("("+data+")")
- JavaScript学习--Item6 var预解析与函数声明提升(hoist )
- 网页中先全部解析JavaScript,(若有事件/入口函数)再调用
- javascript eval函数解析json数据时为什加上圆括号eval("("+data+")")
- javascript解析xml字符串的函数
- 深入解析JavaScript中的立即执行函数
- [转]javascript eval函数解析json数据时为什加上圆括号eval("("+data+")")
- javascript中函数柯里化解析