原型
在 JavaScript 中,有两个原型,分别是 prototype 和 _proto_注:在 ECMA-262 第 5 版中管这个 _proto_ 叫 [[Prototype]]
prototype 属性:这是一个显式原型属性,只有函数才拥有该属性。_proto_ 属性:这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。
什么是函数?什么是对象?
创建函数有两种方式:①、通过 function 关键字定义②、通过 new Function 其中函数又可以分为普通函数和构造函数,两者唯一的区别就是调用的方式不同,语法上没有差异
function normalFn(){} // 普通函数(函数名首字母小写)
function StructFn(){} // 构造函数(函数名首字母大写)
创建对象的方式多种多样,其中传统方法是通过构造函数来创建对象,使用 new 关键字即可创建
let obj = new StructFn() // obj 就是一个对象
在 JS 中,万物都是对象,函数也属于对象,只不过函数相对于对象有着更为精确的定义
原型初探
为了证明 prototype 属性 是函数独有,而__proto__是每个对象都有的,我们可以测试以下代码:
function a(){}
console.log(a.prototype); // {constructor: ƒ}
console.log(a.__proto__); // ƒ () { [native code] }
let obj = new Object()
console.log(obj.prototype) // undefined
console.log(obj.__proto__) // {constructor: ƒ, __defineGetter__: ƒ, …}
可以看到对象的 显式原型 (prototype) 为 undefined
* 注意:undefined 和 null 同属于对象,但是它们都没有原型,为什么?在 JavaScript 中,目前只有两个只有一个值的数据类型,那就是 undefined 和 null* 由于这两种数据类型有且只有一个值,并且没有方法,所以自然而然就没有原型了。有的同学会问,那 NaN 呢?NaN 是属于 Number 数据类型的,属于对象,只有_proto_ 属性
console.log(undefined.__proto__); // 报错:Uncaught TypeError: Cannot read property ‘__proto__’ of undefined
console.log(null.__proto__); // 报错:Uncaught TypeError: Cannot read property ‘__proto__’ of null
console.log(NaN.__proto__) ; // Number {0, constructor: ƒ, toExponential: ƒ, …}
构造函数创建对象,其中发生什么?
1. 创建了一个新对象
2. 将新创建的对象的隐式原型指向其构造函数的显式原型。
3. 将 this 指向这个新对象
4. 返回新对象
注意看第二条:将新创建的对象的隐式原型指向其构造函数的显式原型也就是说 对象.__prototype === 构造函数.prototype
function fn(){}
let obj = new fn();
console.log(obj.__proto__ === fn.prototype); // true
那么这样,我们在为构造函数添加原型方法时,就可以通过两种方法取访问了
fn.prototype.more = function(){console.log(‘fn-prototype-more’)}
// 1、通过构造函数的显式原型
fn.prototype.more()
// 2、通过对象的隐式原型
obj.__proto__.more()
原型链
原型链:实例与原型之间的链接
先来看一个例子:
function Abc(){}
Abc.prototype.fn = function(){console.log(“Abc-prototype-fn”)};
let obj = new Abc()
obj.fn() // Abc-prototype-fn
从上面的代码我们可以看到。构造函数 Abc 和对象实例 obj 都没有 fn 这个方法,之所以对象 obj 能够访问到 Abc 的原型方法,是通过原型链来寻找,借用了 _proto_ 这个属性
所以以下的代码也是等价的
obj.fn() // Abc-prototype-fn
obj.__proto__.fn() // Abc-prototype-fn
再来看一个复杂的例子:
function Abc(){
this.fn = function(){console.log(“Abc-fn”)};
}
Abc.prototype.fn = function(){console.log(“Abc-prototype-fn”)};
Object.prototype.fn = function(){console.log(“Object-fn”)};
let obj = new Abc()
obj.fn = function(){console.log(“obj-fn”)};
obj.fn()
这里有 4 个重名的 fn 函数,分别是:①、实例对象 obj 的方法②、构造函数 Abc 的方法③、构造函数 Abc 原型上的方法④、Obecjt 对象原型上的方法
为了表示方法的寻找过程,我画了一幅很丑陋的图,大家不要介意哈!
在寻找某个方法或者属性的时候,会先从自身对象寻找;如果没有,则会去构造函数寻找;注意这里还没用到原型链;紧接着,会到构造函数的原型上寻找,此时就是对象 Abc 通过 _proto_ 属性进行寻找接下来,会到 Object 对象的原型寻找,Object 对象是所有对象之父,可以说所有的对象都继承了 Object,此时构造函数通过_proto_ 属性找到了 Object 的 prototype 最后的最后,由于 Object._proto_ 指向了 null,这也就是原型链的末端第一道原型链是 obj._proto_ , 访问到了 Abc 的 prototype
console.log(obj.__proto__ === Abc.prototype) // true
第二道原型链是 obj.__proto__.__proto__ , 访问到了 Object 的 prototype
console.log(obj.__proto__.__proto__ === Object.prototype) //true
第三道原型链是 obj.__proto__.__proto__.__proto__ 访问到了 Object 的 prototype.__proto__,,最后指向了 null
console.log(obj.__proto__.__proto__.__proto__ === null) //true
这样我们就可以看到了整个原型链的流程了