重学原型链
一切源于一个简单的案例:
function Foo(){
this.name = name; // 原始属性or方法
}
Foo.prototype.say = function(){ // 原型属性or方法
f
}
var f1 = new Foo();
f1.say(); //hello
Foo.prototype = {};
var f2 = new Foo();
f1.say(); // hello
f2.say(); //报错
本身是一个很简单的关于原型的例子,可我突然就不明白(也许我中了邪)了为什么f2不能访问say方法了。于是乎怀着学习的态度(气愤的无处安放)重学了原型链相关的内容。
写在前面
为了方便描述 我们把定义在方法体内的属性和方法称之为原始属性和方法。把定义在原型上的属性和方法称之为原型属性和方法
原型(原型对象)与原型链
提起原型不得不把原型的‘家谱’摆上,请看图:
把家谱看的明白才能了解家族的历史。所谓原型就是每个实例对象(object)都有一个__proto__属性,该属性指向构造函数的prototype属性,这个prototype对象也叫做原型对象。举个栗子:
function Person(){};
var p = new Person();
p.__proto__ === Person.prototype //true
其实Person本身也是个对象(Function的一个实例),所以Person.__proto__又指向了Object.prototype,而Object.prototype指向null(到头了),这就形成了一个链条也就是所谓的原型链。就像是这样
new操作符
使用new操作符实例化一个对象是很常见的操作,那么new做了哪些事情呢,我们先写一个例子:
function Test(name){
this.name = name;
}
Test.prototype.print = function(){
console.log(this.name);
}
var t = new Test('tom');
- 创建一个空对象 var t = {}
- 把第一步创建的空对象作为上下文执行构造函数,类似这样 Test.call§
- 把t.proto 指向 Test.prototype(获取原型对象上的属性和方法)
- 返回对象t
这里有两个地方需要注意
1. 上述第三步中‘指向’的意思是相当于把当前 Test.prototype 的地址复制给实例对象t,后续如果修改了 Test.prototype的指向,也不会影响修改前创建的实例。这也就是为什么本文最开始提到的例子中修改Foo.prototype = {}后 f1仍能调用say方法的原因。画个图:
2. 如果构造函数有返回值且返回的是对象类型(如 object,date,array)则创建的实例为构造函数的返回值。例如这样
function Test(name){
this.name = name
return {age:18}
}
var t = new Test('tom')
t ==> {age: 18} 而不是 {name: 'tom'}
继承
如果试图引用对象(实例instance)的某个属性,会首先在对象内部寻找该属性,直至找不到,然后才在该对象的原型(constructor.prototype)里去找这个属性.
提到原型链就不得不提js里的继承
原型链继承
function Person(name,age){}
Person.prototype.introduce = function(){
console.log('hello world');
}
Person.prototype.money = [100,200,300];
function Tom(address){}
Tom.prototype = new Person();
Tom.prototype.constructor = Tom;
var tom = new Tom();
tom.introduce(); // hello world
tom.money // [100,200,300]
通过原型链继承tom实例可以访问到Person的原型属性和方法,但是这种方法有个致命的缺点是所有的实例对象都可以访问且修改Person原型上的属性,而原型上的属性是引用类型时实例间的操作就会相互影响,实际开发中并不希望这样的事情发生,接上面的例子:
tom.money[0] = 0;
var tom2 = new Tom();
tom2.money // [0,200,300]
新初始化的tom2的money属性变成了tom修改后的值。
构造函数继承
为了解决原型链继承的弊端,我们可以使用构造函数继承。
function Person(){
this.money = [100,200,300]
this.say = function(){
console.log('hello world');
}
}
Person.prototype.run = function(){
console.log('run');
}
function Tom(){
Person.call(this) //构造函数继承
}
var tom = new Tom();
var tom2 = new Tom();
tom.money[0] = 0;
tom2.money // [100,200,300]
tom2.run() // 报错
这种方法解决了原型链继承的缺点,可是这种方法不能继承原型属性和方法.造成内存上的浪费。
组合继承
function Person(){
this.money = [100,200,300]
}
Person.prototype.run = function(){
console.log('run');
}
function Tom(){
Person.call(this) //构造函数继承
}
Tom.prototype = new Person(); //原型链继承
Tom.prototype.constructor = Tom;
这种方法虽然父类构造函数被调用两次(实例对象中存在两份父类的属性),但是他融合了原型链继承和构造函数继承的优点。点赞!
寄生继承
function Person(){
this.money = [100,200,300];
}
Person.prototype.run = function(){
console.log('run');
}
function Tom(){
Person.call(this); // 通过构造函数继承实现继承父类的原始属性和方法
}
Tom.prototype = Object.create(Person.prototype)
// 该方法会使用指定的原型对象去创建一个新的对象来实现继承父父类的原型方法和属性
Tom.prototype.constructor = Tom;
这种方法解决了组合继承内存浪费的缺点,大大的赞!!
以上就是这一次脑袋中邪后重学原型链的相关,特发此博文以示纪念!