引言
众所周知,JS是一门面向对象的语言,提到面向对象,也许会让人联想到 继承,封装,多态等概念,但是,千万不要带着Java的思维来学JavaScript,尽管他们是哥俩。JS面向对象不同于一些后台语言,其核心机制是原型链。在探究这个机制之前我们还是来明确几个概念。
基本概念:
- 对象,JavaScript的对象是一种无序的集合数据类型,它由若干键值对组成。
- 类,可用来构建对象的函数,也可以叫构造函数
- 实例,由类或构造函数创建的对象
- 函数的原型(prototype),正常情况下,每个函数都会带有一个原型对象
- 对象的原型([[prototype]]/_ proto _ ),指向对象所属类的原型
函数的3种角色:
- 1.普通对象,直接调用属性(Object.keys())
- 2.普通函数,直接执行(Object())
- 3.构造函数(new Object())
特殊情况:
- 没有原型的函数是不可以 new执行的
- 没有原型的函数:1.箭头函数 2.采用ES6对象成员快捷写法的函数 3.在class内书写,当作公共属性放在类原型上的函数
- 在class的写法下,原型上的方法通过实例调用和原型调用所输出的this是不同的
new A().fn() ,fn的this是A的实例,A.prototype.fn() , fn的this是构造函数A
构造函数执行过程
构造函数执行相比普通函数有一些不同:
- 1.初始化作用域链
- 2.创建一块堆内存空间(不同)
- 3.初始化this,this指向2创建的那块空间
- 4.初始化arguments
- 5.形参赋值
- 6.变量提升
- 7.代码执行
-
- 这时对this的所有操作都作用在开辟的堆内存上(不同)
若代码中没有返回值或返回原始值,最终会把创建的堆内存地址返回出去(不同)
- 这时对this的所有操作都作用在开辟的堆内存上(不同)
new 执行构造函数,得到的实例对象的[[prototype]]属性指向 构造函数的prototype,这个就是原型链。当访问一个实例对象内不存在的属性,JS引擎就会沿着原型链向上寻找
举个例子:
function fn (name,age) {
this.name = name
this.age = age
}
let obj = new fn('jack',25)
fn 作为构造函数 new 执行所创建的obj对象,其原型链如下:
js的多种继承
1.原型继承:
Child.prototype = new Parent
子类原型上有父类公有的和私有的
2.call继承
Child(){ Parent.call(this) }
子类实例包含父类私有的,但找不到父类公有的
3.寄生组合继承(call继承 + 修改原型指向 )
Child(){ Parent.call(this) }
Child.prototype=Object.assign(Object.create(Parent.prototype),Child.prototype)
父类私有的也是子类私有的,公有的也是子类公有的
4.ES6的class继承
class Child extends Parent {}
子类必须把super()写在constructor内第一行
Child.prototype.__proto__自动指向Parent.prototype
Child._ proto _自动指向Parent
区分[[prototype]] 和 _ proto _
一般情况下,在同一对象内的这两者都是指向同一结果(当前对象所属类的原型)。但是,如果都一样的话为什么要存在2个形式呢?
首先,我们获取对象原型的方式有两种:
Object.getPrototypeOf(XXX)
XXX._ _ proto _ _关于_ proto _ 其实最开始是浏览器厂商先实现的,之后ECMA262规范发现好多厂商都实现了这个API,所以才加入规范里。
进一步了解_ _ proto_ _
我们可以通过
_ _ proto_ _来访问对象的原型,似乎给了我们一种错觉,_ _ proto_ _是对象的属性,但其实不是的,_ _ proto_ _是位于Object.prototype上的访问器属性(accessor property)。只有继承了这个原型的对象才能使用_ _ proto_ _获取原型。
let obj1= Object.create(null) / / 得到一个纯净的对象,没有原型也没有任何属性
let obj2= Object.create(obj1) / / 此时obj2不能通过__proto__ 访问到它的原型obj1
原型重定向
既然我们知道了原型链的原理,那么我们就可以通过修改对象的原型链来给他扩展一些公共方法(根据MDN的文档,这样很耗费性能,不建议这么做)
举例:
let obj = {}
obj.__proto__ = Function.prototype
这样我们就可以拿到 call apply bind 等方法了,但是这并不意味着obj变成了函数因为obj 不具有`[[call]]`属性
有趣的现象:
- 箭头函数是没有原型的,所以他不能作为构造函数 new执行,但是,原因真的这么简单吗?其实箭头函数不能new执行是因为他没有
[[constructor]]属性- 如果我们创建了一个函数fn,然后让
fn.prototype = null,那么new fn创建的对象的[[prototype]]指向的是Object.prototype
函数的创建过程
要想再深入的探究下去,我们需要去看看官方文档对函数创建的描述,但是由于规范的更新,很多东西出现了变动,所以我截取了ES5,和ES8的规范进行对比。
ES5 的描述
ES8 的描述
参考
[1] stackoverflow.com/questions/6…
[2] stackoverflow.com/questions/4…
[3] developer.mozilla.org/zh-CN/docs/…