快速理解原型链

JS中原型链的理解

一、了解构造函数、原型和实例之间的关系( 实例原型三角形 )

1
2
3
在 js 中凡是函数背后都有一个对象存在.
这个对象使用 函数名.prototype 来访问. 默认情况下这个对象含有一个属性 constructor用于指向回该函数.
也就是说, 在默认情况下, 如果一个对象含有 constructor 属性, 同时,它执行某一个函数, 那么就表示这个对象是这个函数背后的 prototype.
  • 在构造函数上,都有一个 原型属性 prototype,该属性也是一个对象( Object 的实例 );
  • 原型对象上有一个 constructor 属性,该属性指向 原型对象所属的构造函数 ;
  • 而实例对象上也有一个 __proto__ 属性,该属性也指向 构造函数的原型属性 ( 即指向自己的原型对象 ),它是一个非标准属性,不可以用于编程,它是浏览器使用,便于快速访问查看实例的原型属性。
1
2
3
4
5
6
7
// __proto__
在函数里有一个属性 prototype
//prototype 与 __proto__的关系
☞ __proto__ 是站在对象角度来说的
☞ prototype 是站在构造函数来说的.
函数的 prototype 这个对象, 里面的所有成员( 属性, 方法 )
都会默认的连接到这个函数作为构造函数时创建的对象上.

看图理解函数、原型和实例之间的关系( 绘制原型实例三角形 )

插入图片

原型实例三角形

注意:
  1. 凡是构造函数就有原型属性, 凡是实例对象就有原型对象.
  2. prototype 和 proto都指向同一个对象。 这个对象在构造函数角度来看, 是利用 prototype 属性获得到的, 因此将其称为 构造函数的 原型属性, 简称原型.如果站在实例对象和这个神秘对象的角度来看, 神秘对象被称为 实例对象的 原型对象, 简称为原型.

二、什么是原型链

说白了,其实就是有限的实例对象和原型之间组成有限链,用来实现 属性共享 和 继承。

1
2
3
4
5
6
//原型链示例
var arr = [];
arr -> Array.prototype ->Object.prototype ->null
var o = new Object();// var o = {};
o -> Object.prototype -> null;

三、接下来探究 继承 问题

1
概念:所谓的继承就是拿来主义,自己没有,别人有,拿来称为自己的,就表示你继承了这个东西。

实现继承的方法:

1. 原型继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Animal(name){
this.name = name;
}
function Tiger(color){
this.color = color;
}
// var tiger = new Tiger('yellow');
// console.log(tiger.color);
// console.log(tiger.name); //undefined
// Tiger.prototype = new Animal('老虎');
// 第一种方式
Object.prototype.name = '大老虎'; //第二种方式
var tiger = new Tiger('yellow');
console.log(tiger.color);
console.log(tiger.name);

2. 直接替换原型对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Person.prototype = {
constructor: Person,
sayHello: function () {},
run: function () {},
eat: function () {},
walk: function () {}
};
注意: 这个方法实际上更换的原有的原型对象. 应该注意.
function Person() {}
var p1 = new Person();
// 继承关系: p1 -> 原始的原型( Person.prototype ) -> Object.prototype -> null
Person.prototype = {
constructor: Person,
sayHello: function () {},
run: function () {},
eat: function () {},
walk: function () {}
};
var p2 = new Person();
// 继承关系: p2 -> 新的原型( 含有 sayHello 等方法的原型 ) -> Object.prototype -> null
注意:
  • 它不方便给父级类型传递参数;
  • 父级类型当中的引用类型被所有实例共享

3. 标准继承:利用 Object.creat()方法 实现继承

1
2
3
4
5
6
7
8
9
10
//兼容
function create(obj){
if(Object.create){
return Object.create(obj);
}else{
function Foo(){}
Foo.prototype = obj;
return new Foo();
}
}
注意:
Object.create() 方法使用指定的原型对象和其属性创建了一个新的对象。它是 ES5 的新特性。

语法:
Object.creat( 对象 ) -> 新的对象

意义:
返回的新对象,原型继承自 create 方法参数中提供的对象。

用途:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
-> 1.借用已有方法实现原型式继承
场景: 数组不能遍历键值对, 我自己创建有一个构造函数,
使得我可以处理键值对,将键值对作为有序键值对来使用.
例如:
var kv = { name: 'jim', age: 19, gender: '男' };
例如我创建一个构造函数 SortedList, 可以将其转换成
[ { key: 'name', value: 'jim' },
{ key: 'age', value: 19 },
{ key: 'gender', value: '男' } ]
再提供一个 keys 方法与 values 方法, 分别获得所有的键 或 值
我们可以认为 SortedList 是继承自数组的对象.
实现以下:
function SortedList ( kv ) {
// this 继承自 数组, 因此 this 应该就是数组,
// 因此就应该将 kv 中每一个键值对构造成一个对象以元素的形式存储在 this 中
for ( var key in kv ) {
this.push({
key: key,
value: kv[ key ]
});
}
}
// SortedList.prototype = Object.create( Array.prototype );
// 简化一下
SortedList.prototype = [];
-> 2.提高缓存性能
var cache = {}; // 缓存
function __ajax__( config ) {
var data = config.data;
// data 是一个键值对
// 将键值对拼成 请求参数
var tmp = [];
for ( var key in data ) {
tmp.push( key + '=' + data[ key ] );
}
var queryString = '?' + tmp.join( '&' );
console.log( queryString );
}


4. 借用构造函数继承:

1
2
3
4
5
6
7
8
9
10
11
//被借用的构造函数中原型上的成员没有被拿来
function Animal(name){
this.name = name;
}
function Mouse(nickname){
Animal.call(this,'老鼠');
this.nickname = nickname;
}
var m = new Mouse('杰瑞');
console.log(m.name);
console.log(m.nickname);
存在的问题:

可以解决原型继承中的传参问题,但是父类型当中的原型对象上的成员 ( 属性和方法 ) 不能被继承到。

5. 混入式继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function extend( obj ) {
for ( var k in obj ) {
this[ k ] = obj[ k ];
}
}
jQuery.extend({
map: function () {},
each: function () {}
});
jQuery.fn.extend({
map: function () {},
each: function () {}
});

6. 组合继承:

1
2
3
4
5
6
7
8
组合: 多个东西放到一起.
组合式继承就是说把多个继承方案混合到一起使用.
就以原型式继承与混入式继承为例, 一般书写构造函数的时候都是这么实现的:
1> 写一个构造函数
2> 准备一个原型
3> 准备一个混入方法
4> 在原型中混入多个成员
5> 使用构造函数创建对象, 对象就有 多个 方法了.

四、属性搜索原则 和 写入原则

属性搜索原则

  1. 在访问对象的某个成员的时候会先在当前对象中查找是否存在,如果当前对象存在,停止查询;
  2. 如果当前对象中没有,就在构造函数的原型对象中查找,如果存在,停止查询;
  3. 如果原型对象中没有找到,就到原型对象的原型中查找…
  4. 直到找到或者查询到Object的原型对象的原型是 null 为止。

写入原则

获取 一个对象的属性值或方法的时候,才会沿着原型链向下寻找, 属性赋值 没有这个,如果是给对象设置成员( 属性或方法 ),都是在当前对象上进行设置。

1
2
3
4
5
6
7
8
9
10
var obj1 = {name:'one'};
obj2 = Object.create(obj1);
obj2.name = 'two';
console.log(obj1.name);
//one
var obj1 = {prop:{name:'one'}};
obj2 = Object.create(obj1);
obj2.prop.name = 'two';
console.log(obj1.prop.name);
//two

五、拓展

proto属性

1
2
3
4
5
最早是 狐火在浏览器中引入, 其目的是利用实例对象观察其原型的结构.
早期没有 __proto__ 的时候, 为了观察对象的继承关系, 必须通过 实例.constructor.prototype
来获得实例对象的 原型对象. 在分析过程中非常麻烦. 因此在 火狐浏览器中给实例对象引入了
__proto__ 属性, 用于快速的访问该实例的 原型对象. 该属性带有双下划线, 表示内部属性.
后来各大浏览器也效仿, 引入该属性, 从而使用 利用实例也可以访问原型对象了( 神秘对象了 ).
1
2
3
4
//如果浏览器不支持 __proto__ 我们应该怎么实现该功能呢?
Object.prototype.__myProto__ = function () {
return this.constructor.prototype;
};

Object.prototype 相关

Object.prototype中常用成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-> hasOwnProperty
语法:
对象.hasOwnProperty( '属性的名字' ) -> boolean
含义:
当前对象是否含有该属性, 意味着该属性不是原型继承而来.
-> isPrototypeOf
语法:
对象1.isPrototypeOf( 对象2 ) -> boolean
含义:
对象1 是不是 对象2 的原型对象, 如果是 返回 true, 否则返回 false
-> propertyIsEnumerable
语法:
对象.prototypeIsEnumerable( '属性名' ) -> boolean
含义:
对象的对应属性 如果是 自己的( 非原型中的 ) 同时可枚举( 可以 forin 出来 ), 就返回 true, 否则返回 false
注意:
所谓的 可枚举是说可以被 for-in 遍历出来
在 ES5 以前, 用户自定义的属性方法都是可以枚举的, 不允许设置为不可枚举.
在 ES5 以后, 引入了 Object.defineProperty 和 Object.defineProperties 方法, 用该方法可以定义其不可枚举.
-> toLocaleString 和 toString
to*String 作用是将对象转换成字符串, 以供打印输出.
由于每一个对象都可以调用 to*String 方法, 但是又不清楚如何实现字符串的转换
因此 js 的作者就约定 默认的 to*String 显示出: [object 构造函数名]
如果是定义构造函数则显示 [object Object]. 然后由各个对象自己实现具体 to*String