共计 6060 个字符,预计需要花费 16 分钟才能阅读完成。
写在前面
如果说 JavaScript 是一本武学典籍,那么原型链就是九阳神功。在金庸的武侠小说里面,对九阳神功是这样描述的:
“ 练成「九阳神功」后,会易筋洗髓;生出氤氲紫气;内力自生速度奇快,无穷无尽,普通拳脚也能使出绝大攻击力;防御力无可匹敌,自动护体,反弹外力攻击,成就金刚不坏之躯;习者速度将受到极大加成;更是疗伤圣典,百病不生,诸毒不侵。至阳热气全力施展可将人焚为焦炭,专门克破所有寒性和阴毒内力。” 可见其功法强大。
那么,如何修炼好 js 中的九阳神功呢?真正的功法大成的技术是从底层上去理解,那种工程师和码农的区别就在于对底层的理解,当你写完一行代码,或者你遇见一个 bug, 解决的速度取决于你对底层的理解。什么是底层?我目前个人的理解是“在你写每一行代码的时候,它将如何在相应的虚拟机或者 V8 引擎中是如何运行的,更厉害的程序员甚至知道每条数据的操作是在堆里面还是在栈里面,都做到了然于胸,这是 JavaScript 的内功最高境界(反正我现在是做不到,我不知道你们能不能,哈哈哈)”。
一、Js 原型链的简单理解
** 理解原型链之前首先要了解 js 的基本类型和引用类型:
1、基本类型
基本类型有 Undefined、Null、Boolean、Number 和 String。这些类型在内存中分别占有固定大小的空间,他们的值保存在栈空间,
我们通过按值来访问的。
基本类型:简单的数据段,存放在栈内存中,占据固定大小的空间。
2、引用类型
引用类型,值大小不固定,栈内存中存放地址指向堆内存中的对象。是按引用访问的 。
存放在堆内存中的对象,变量实际保存的是一个指针,这个指针指向另一个位置。每个空间大小不一样,要根据情况开进行特定的分配。
当我们需要访问引用类型(如对象,数组,函数等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。**
js 的原型链说简单也简单,说难也难。
首先说明:函数(Function)才有 prototype 属性,对象(除了 Object)拥有_proto_.
原型链的顶层就是 Object.prototype, 而这个对象的是没有原型对象的。
可以在 Chrome 输入:
Object.__proto__
输出的是:
ƒ () { [native code] }
可以看到这个没有.prototype 属性。
二、prototype 和_proto_的区别
我们知道原型是一个对象,其他对象可以通过它实现属性继承。
var a = {};
console.log(a.prototype); //undefined
console.log(a.__proto__); //Object {}
var b = function(){}
console.log(b.prototype); //b {}
console.log(b.__proto__); //function() {}
/*1、字面量方式 */
var a = {};
console.log(a.__proto__); //Object {}
console.log(a.__proto__ === a.constructor.prototype); //true
/*2、构造器方式 */
var A = function(){};
var a = new A();
console.log(a.__proto__); //A {}
console.log(a.__proto__ === a.constructor.prototype); //true
/*3、Object.create()方式 */
var a1 = {a:1}
var a2 = Object.create(a1);
console.log(a2.__proto__); //Object {a: 1}
console.log(a.__proto__ === a.constructor.prototype); //false(此处即为图 1 中的例外情况)
var A = function(){};
var a = new A();
console.log(a.__proto__); //A {}(即构造器 function A 的原型对象)console.log(a.__proto__.__proto__); //Object {}(即构造器 function Object 的原型对象)console.log(a.__proto__.__proto__.__proto__); //null
instanceof 究竟是运算什么的?
我曾经简单理解 instanceof 只是检测一个对象是否是另个对象 new 出来的实例(例如 var a = new Object(),a instanceof Object 返回 true),但实际 instanceof 的运算规则上比这个更复杂。
// 假设 instanceof 运算符左边是 L,右边是 R
L instanceof R
//instanceof 运算时,通过判断 L 的原型链上是否存在 R.prototype
L.__proto__.__proto__ ..... === R.prototype?
// 如果存在返回 true 否则返回 false
注意:instanceof 运算时会递归查找 L 的原型链,即 L.__proto__.__proto__.__proto__.__proto__… 直到找到了或者找到顶层为止。
所以一句话理解 instanceof 的运算规则为:
instanceof 检测左侧的__proto__原型链上,是否存在右侧的 prototype 原型。
图解构造器 Function 和 Object 的关系
我们再配合代码来看一下就明白了:
//①构造器 Function 的构造器是它自身
Function.constructor=== Function;//true
//②构造器 Object 的构造器是 Function(由此可知所有构造器的 constructor 都指向 Function)Object.constructor === Function;//true
//③构造器 Function 的__proto__是一个特殊的匿名函数 function() {}
console.log(Function.__proto__);//function() {}
//④这个特殊的匿名函数的__proto__指向 Object 的 prototype 原型。Function.__proto__.__proto__ === Object.prototype//true
//⑤Object 的__proto__指向 Function 的 prototype,也就是上面③中所述的特殊匿名函数
Object.__proto__ === Function.prototype;//true
Function.prototype === Function.__proto__;//true
当构造器 Object 和 Function 遇到 instanceof
Function.__proto__.__proto__ === Object.prototype;//true
Object.__proto__ === Function.prototype;//true
如果看完以上,你还觉得上面的关系看晕了的话,只需要记住下面两个最重要的关系,其他关系就可以推导出来了:
1、所有的构造器的 constructor 都指向 Function
2、Function 的 prototype 指向一个特殊匿名函数,而这个特殊匿名函数的__proto__指向 Object.prototype
function、Function、Object 和{}
我们知道,在 Js 中一切皆为对象(Object),但是 Js 中并没有类(class);Js 是基于原型(prototype-based)来实现的面向对象(OOP)的编程范式的,但并不是所有的对象都拥有 prototype 这一属性:
var a = {};
console.log(a.prototype); //=> undefined
var b = function(){};
console.log(b.prototype); //=> {}
var c = 'Hello';
console.log(c.prototype); //=> undefined
prototype 是每个 function 定义时自带的属性,但是 Js 中 function 本身也是对象,我们先来看一下下面几个概念的差别:
function 是 Js 的一个关键词,用于定义函数类型的变量,有两种语法形式:
function f1(){console.log('This is function f1!');
}
typeof(f1); //=> 'function'
var f2 = function(){console.log('This is function f2!');
}
typeof(f2); //=> 'function'
如果用更加面向对象的方法来定义函数,可以用 Function:
var f3 = new Function("console.log('This is function f3!');");
f3(); //=> 'This is function f3!'
typeof(f3); //=> 'function'
typeof(Function); //=> 'function'
实际上 Function 就是一个用于构造函数类型变量的类,或者说是函数类型实例的构造函数(constructor);与之相似有的 Object 或 String、Number 等,都是 Js 内置类型实例的构造函数。比较特殊的是 Object,它用于生成对象类型,其简写形式为{}:
var o1 = new Object();
typeof(o1); //=> 'object'
var o2 = {};
typeof(o2); //=> 'object'
typeof(Object); //=> 'function'
prototype VS_proto_
prototype 和 length 是每一个函数类型自带的两个属性,而其它非函数类型并没有(开头的例子已经说明),这一点之所以比较容易被忽略或误解,是因为所有类型的构造函数本身也是函数,所以它们自带了 prototype 属性:
除了 prototype 之外,Js 中的所有对象(undefined、null 等特殊情况除外)都有一个内置的 [[Prototype]] 属性,指向它“父类”的 prototype,这个内置属性在 ECMA 标准中并没有给出明确的获取方式,但是许多 Js 的实现(如 Node、大部分浏览器等)都提供了一个__proto__属性来指代这一[[Prototype]],我们通过下面的例子来说明实例中的__proto__是如何指向构造函数的 prototype 的:
var Person = function(){};
Person.prototype.type = 'Person';
Person.prototype.maxAge = 100;
var p = new Person();
console.log(p.maxAge);
p.name = 'rainy';
Person.prototype.constructor === Person; //=> true
p.__proto__ === Person.prototype; //=> true
console.log(p.prototype); //=> undefined
图示解释上面的代码:
Person 是一个函数类型的变量,因此自带了 prototype 属性,prototype 属性中的 constructor 又指向 Person 本身;通过 new 关键字生成的 Person 类的实例 p1,通过__proto__属性指向了 Person 的原型。这里的__proto__只是为了说明实例 p1 在内部实现的时候与父类之间存在的关联(指向父类的原型),在实际操作过程中实例可以直接通过. 获取父类原型中的属性,从而实现了继承的功能。
核心图解
var Obj = function(){};
var o = new Obj();
o.__proto__ === Obj.prototype; //=> true
o.__proto__.constructor === Obj; //=> true
Obj.__proto__ === Function.prototype; //=> true
Obj.__proto__.constructor === Function; //=> true
Function.__proto__ === Function.prototype; //=> true
Object.__proto__ === Object.prototype; //=> false
Object.__proto__ === Function.prototype; //=> true
Function.__proto__.constructor === Function;//=> true
Function.__proto__.__proto__; //=> {}
Function.__proto__.__proto__ === o.__proto__.__proto__; //=> true
o.__proto__.__proto__.__proto__ === null; //=> true
从上面的例子和图解可以看出,prototype 对象也有__proto__属性,向上追溯一直到 null
new 关键词的作用就是完成上图所示实例与父类原型之间关系的串接,并创建一个新的对象;instanceof 关键词的作用也可以从上图中看出,实际上就是判断__proto__(以及__proto__.__proto__…)所指向是否父类的原型:
var Obj = function(){};
var o = new Obj();
o instanceof Obj; //=> true
o instanceof Object; //=> true
o instanceof Function; //=> false
o.__proto__ === Obj.prototype; //=> true
o.__proto__.__proto__ === Object.prototype; //=> true
o.__proto__.__proto__ === Function; //=> false
原型链的结构
1. 原型链继承就是利用就是修改原型链结构(增加、删除、修改节点中的成员), 从而让实例对象可以使用整个原型链中的所有成员(属性和方法)
2. 使用原型链继承必须满足属性搜索原则
属性搜索原则
1. 构造函数 对象原型链结构图
function Person (){}; var p = new Person();
2.{} 对象原型链结构图
3. 数组的原型链结构图
4.Object.prototype 对应的构造函数
总结:
从本质上理解: 对象和函数都是保存在堆当中的引用类型, 后面一系列的操作都是为了使用或者访问其属性, 那么无论是 prototype 还是_proto_都是函数或者 Object 自带的指针,允许外界的其他一些函数或者 Object 去使用自己的一些属性。
更多的文章请关注公众号:码客小栈,每天不定时的更新 web 好文