类和原型
JavaScript也是一门面向语言。面向对象,但它与类无关。
受之前学习java的影响。很长一段时间,我觉得“对象”和“类”绑定在一起的。
但对象的实现源于继承,通过类只是一种方法,另一种是原型。
原型本质上是一个对象
在JavaScript中,所有的对象都有一个属性,叫proto。这是JavaScript引擎内置的,即一个对象的原型。
在控制台随便打印一个对象都可以看到一个灰色的__proto__
下划线是为了防止开发者也自定义一个属性叫proto,将内置的proto覆盖。
proto作用和类一样,实现继承,实现创造对象,但它更简洁
通过原型来继承(prototypal inheritance)
继承:
inheritance, one object gets access to the properties and methods of another object.
继承就是一个对象获取另一个对象的属性和方法
只说缺点:通过类来继承太冗长。一个类往往会被被private、protected、public之类的词修饰,proto通过原型继承简洁很多。
尝试改变对象的原型
举个栗子。我们有两个对象,一个叫A,另一个叫B。
A有一个属性叫aProperty,我们可以通过A.aProperty找到它;B有一个属性叫bProperty,我们可以通过B.bProperty找到它。
如果写A.bProperty,当然不行,因为A没有bProperty这个属性。
但如果我们将A的原型指定为B(A.__proto__ = B)。
当我们写A.bProperty,会首先在A自己的属性中去寻找,没有找到。再去他的原型——B中寻找,找到了就可以用了。
不用写成A.__ proto __.bProperty。
A.__proto__ = B只是举个栗子,不要真的这么写,下面会解释为什么。
JSE会自动去A原型中寻找,这样我们就实现了继承,通过将A的__proto__指定为B,我们获得了B的属性和方法。
如果指定B的原型为C,我们还可以写A.cProperty——没能在A中找到的属性,去A的原型B中找,没能在B中找到的属性,去C中找...
一层一层往一个对象的原型找找下去,所以有个词语,叫原型链(prototype chain)。
改变proto指向,实现继承
var person = {
firstname: 'Default',
lastname: 'Default',
getFullName: function() {
return this.firstname + ' ' + this.lastname;
}
}
var john = {
firstname: 'John',
lastname: 'Doe'
}
console.log(person);
console.log(john);
将john的原型设置为person后
var person = {
firstname: 'Default',
lastname: 'Default',
getFullName: function() {
return this.firstname + ' ' + this.lastname;
}
}
var john = {
firstname: 'John',
lastname: 'Doe'
}
john.__proto__ = person;
console.log(john.getFullName());//John Doe //注意:this的指向的是调用它的最近的对象,所以是john
console.log(person);
console.log(john);
展开注释看看john这个对象的__proto__有什么差别
红色箭头指的,叫base object
base object/Object.prototype
之前说所有的对象都有原型(proto),其实不太对,base object是唯一的一个例外。它没有原型。
var a = {};
var b = function(){};
var c = [];
在控制台试试下面的属性
红色箭头所指就是base object,它没有原型了。
解释一下为什么红色箭头所指,不是红色箭头下面那个:
a.__proto__是base object,a.__proto__.__proto__被认为是base object上的一个名为__proto__属性,回车,没有找到,所以打印出null,而不是报错.但如果再继续写a.__proto__.__proto__.__proto__,就是再一个空对象(a.__proto__.__proto__)再去找,自然就报错了。
也就是说,对于a这种最普通的对象,往下找一层就是base object
数组和函数这种特别的对象,往下找两层是base object。
这种指向是by reference,不是by value。就是说只有一块内存存着这个base object。所有的object,最最最最底层都是它。用这个例子证明只有一个base object:
a.__proto__.myProperty = 'brynn';
console.log(b.__proto__.__proto__.myProperty)//brynn
通过a在base object上加一个属性(myProperty),通过b,我们拿到了这个属性
Object.prototype里有什么?
可以看到是一些method,比较常用的是toString,这也是为什么在JavaScript中可以一个对象可以很轻松转化为字符串,它从base object找到的这个方法。
所有的对象都可以找到这个方法。
Function.prototype里有什么?
不知道为什么,我的控制台没法把这一部分展开,里面也是一些方法,常用的有call/bind/apply
Array.prototype里有什么?
可以看到是一些属性和方法,所以对于一个数组,在JavaScript中可以轻松的用.push/.pop/.length...
所有的数组对象都可以找到这些方法。(其实只要是数组就可以,相关:<还没写原始类型和自动装箱>)
通过proto继承:extends
A如果想用B的属性和方法,可以将A的proto设为B。
像这样(A.__proto__ = B)
但并不推荐这样做,这也是为什么proto前后都要加两个下划线——我们可能会取一个属性的名字叫proto,但不太可能叫__proto__:并不 希望开发者手动设置一个对象的原型。
因为这会降低效率——假设A和B都是两个普通的对象。A和B都是在prototype chain的第二层,第一层是base object(即a和b的原型都是Object.prototype)。
A如果用到
Object.prototype里的属性或方法,往下找一层就可以找到Object.prototype。
但如果我们将A的原型设为B,那么,A升级了,它现在是原型链的第三层。
如果现在写要用到Object.prototype里的转字符串方法A.toString,
会先从A自己的属性中找,再去A的原型B中找,再去B的原型Object.prototype。终于找到了。
三层,当然会花费更多的时间。 不让开发者手动设置__proto__的目的,就是让每一个对象都尽可能离Object.prototype近一些。
但如果我们需要在A上用B的属性,我们可以用extends。
在es6之前,并没有extends,各大library和framework会自己来实现这个功能。
摘自underscoreJS的extend:
var createAssigner = function(keysFunc, undefinedOnly) {
return function(obj) {
var length = arguments.length;
if (length < 2 || obj == null) return obj;
for (var index = 1; index < length; index++) {
var source = arguments[index],
keys = keysFunc(source),
l = keys.length;
for (var i = 0; i < l; i++) {
var key = keys[i];
if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];
}
}
return obj;
};
};
看不懂上面的代码也没关系,只要知道:
它实现了将B有的,A没有的属性给了A,并且A和B还是同级的关系。
var a = new Number(3);
var b ={
value:3
};
console.log(a);
Number {3}
__proto__:Number
[[Primitive]]:3
console.log(b);
{value:3}
value:3
__proto__:Object
a是一个特别一点的对象,在prototype chain的第三层,第二层是一个叫Number的proto,最底层才是base object.
b是一个最普通的对象,在prototype chain的第二层;