前言
在开始讲原型链之前我们需要了解与面向对象有关的知识,而原型链部分也要求对原型链的有一定了解(如提前看过JavaScript高级程序设计[第四版]的原型链部分)。
面向对象
什么是[面向对象]概念?
要明白面向对象,首先要理解什么是面向过程,面向过程是我们一开始接触编程时的编程思路,比如学C语言的时候。
面向过程(Procedure Oriented 简称PO :如C语言):
从名字可以看出它是注重过程的。当解决一个问题的时候,面向过程会把事情拆分成: 一个个函数和数据(用于方法的参数) 。然后按照一定的顺序,执行完这些方法(每个方法看作一个过程),等方法执行完了,事情就搞定了。
面向对象(Object Oriented简称OO :如C++,JAVA等语言):
看名字它是注重对象的。当解决一个问题的时候,面向对象会把事物抽象成对象的概念,就是说这个问题里面有哪些对象,然后给对象赋一些属性和方法,然后让每个对象去执行自己的方法,问题得到解决。
可看: 2分钟让你明白什么是面向对象编程
面向对象的三大特性
封装
封装,就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。比如私有属性/方法,通过程序逻辑来设定将哪些类的属性暴露出去。
继承
继承,指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
多态
多态,是指一个类实例的相同方法在不同情形有不同表现形式。这个概念相对抽象,一句话总结就是说:同一个方法被不同的对象执行会有不同的结果。有很多面向对象的语言的API中都体现了“多态”的特征。就算语言API中没有提供多态的接口,最简单的在js方法里面通过if else判断入参也能达到实现[多态]这个特性。
为什么需要面向对象
很明显,面向对象有降低代码耦合度,提高代码复用性等优势。如JAVA就有很多面向对象的特性的API,如继承,方法重载等。这是API层面实现的面向对象。不仅如此,我们在开发过程中也要尽量遵循面向对象的思路(三大特性以及诸多的程序设计原则)去设计我们的程序。
与原型链何干?
好,说到这里,面向对象和我们的原型链有什么关系呢?结合上面这段话我们大概可以想到,原型链就是JavsScript中对于[继承]特性的API实现。
原型链
我们回过头来看看[继承]的概念:继承,指可以让某个类型的对象获得另一个类型的对象的属性的方法。
简单地理解,就是对象A继承了对象B,对象A可以用对象B的属性和方法了。而[原型链]实现了[继承]这一特性。
表现
原型链相关的逻辑说明在网上有非常多的文章。总的来说就是这么一张图。
而原型链本身指的是,一个对象沿着__proto__这个属性往上找,形成一条链路,这个就是原型链。
var Foo = function() {};
var f1 = new Foo;
console.log(f1.__proto__ === Foo.prototype) //true
console.log(Foo.__proto__ === Function.prototype) //true
console.log(Foo.prototype.__proto__ === Object.prototype) //true
Object.prototype.__proto__ === null // true
如何理解原型链?
就从例子中的这个构造函数开始
var Foo = function() {};
var f1 = new Foo;
我们使用new操作符创建了一个A的实例,这个时候f1.__proto__ === Foo.prototype 为 true。
也就是说实例的__proto__指向构造函数的prototype
依据这样的一条特性,我们能推导出上图中大部分的指向关系。
在推导之前,我们需要知道一个前提条件:所有被创建的函数都有protoype属性,对象都有__proto__属性。函数的prototype是一个对象,这个对象在该函数被创建的伊始就存在,它的constructor属性默认指向该函数。我们开始推导吧!
比如Js内置的引用对象(同时也是函数)Function,所有的被创建的function都是Function的实例。所以Foo.__proto__ === Function.prototype 为 true。
甚至连他自己也是,所以Function.__proto__ === Function.prototype 为 true。
再往上,所有被创建的对象都是Object的实例。
这句话有一个例外,就是Object.create()创建出来的对象的__proto__属性并不指向Object.prototype
我们看到Function.prototype.__proto__指向Object.prototype。那么万物的尽头是Object.prototype,它自己也是一个对象,它不能指向它自己吧?于是就把它的__proto__属性赋值为null。
From 原型链 to 继承
我们已经概括完上面这张图究竟是怎么一回事了。得讲讲为什么原型链能实现[继承]了。
const a = {};
a.__proto__ === Object.prototype // true
a.a // undefined
a.toString(); // "[object Object]"
这是一个简单的对象,里面啥都没有,a.a 是undefiend是正常的。为啥toString没问题?
这就是原型链的机制,当访问一个对象内的属性的时候,首先先访问这个对象内可遍历的属性。然后发现没有,就去__proto__指向的对象里面找。再没有,就去__proto__.proto__指向的对象里面去找....直到__proto__指向null。那就不找了。
因此,我们可以将父类放在__proto__链的上游,下游就能够继承上游父类的属性和方法。Object则是最上游,因此所有对象都能访问Object.prototype的可遍历方法和属性,包括toString。
补充
new
[[Construct]]
When the [[Construct]] internal method for a Function object F is called with a possibly empty list of arguments, the following steps are taken:
-
Let obj be a newly created native ECMAScript object.
-
Set all the internal methods of obj as specified in 8.12.
-
Set the [[Class]] internal property of obj to
"Object"
. -
Set the [[Extensible]] internal property of obj to true.
-
Let proto be the value of calling the [[Get]] internal property of F with argument
"prototype"
. -
If Type( proto) is Object, set the [[Prototype]] internal property of obj to proto.
-
If Type( proto) is not Object, set the [[Prototype]] internal property of obj to the standard built-in Object prototype object as described in 15.2.4.
-
Let result be the result of calling the [[Call]] internal property of F, providing obj as the this value and providing the argument list passed into [[Construct]] as args.
-
If Type( result) is Object then return result.
-
Return obj.
首先,说明了[[Construct]]这个内置方法(函数)被执行了之后会做这十件事情。这也是new操作符做的事情,也就是说new操作符调用了构造函数F的内置函数[[Construct]]。
第一步:新建一个ECMA标准对象obj
第二步:按照8.12标准设置obj对象里面的所有内置方法
第三步:设置内置属性[[Class]]为 "Object"
第四步:设置内置属性[[Extensible]]为 true
第五步:让obj的属性——__proto__的值为F的属性——prototype的值
第六步:__proto__如果是对象,那就将 __proto__的值 赋值给 obj的内部属性[[Prototype]]。
第七步:__proto__如果不是对象,那就将 (15.2.4标准的)Object.prototype的值 赋值给 obj的内部属性[[Prototype]]。
拓展
- Instanceof 为什么能判断A是否是B的原型
- 怎么解决[A 和 B 都继承C, A 能沿着原型链修改父类C的属性/方法]这样的问题?
- 常见的继承方式?
- this的指向有什么规则?——this的绑定
- call() bind() apply()的区别,有兴趣可以了解实现原理
- 为什么'dfsfds'.length可以执行
- 面向对象中的'封装',在java中依赖[私有属性]达到目的,js中直到es6+才出现私有属性,而es6+的大部分语法都可以使用es5实现,私有属性的实现思路是什么?
- 了解私有属性之前,得先了解[闭包]
- 作用域链
- 了解私有属性之前,得先了解[闭包]