一切皆对象
JavaScript和面向类的语言不同,它没有类来作为对象的抽象模式,JavaScript中只有对象;也就是咱很喜欢说的’一切皆对象’,其实这句话还有待商榷,毕竟原始类型值也只是个值,其中null
和undefined
还比较神秘;
函数与对象的关系
JavaScript中的Function
总让人产生疑惑:1
2
3
4
5function fn() {
...
}
console.log(typeof fn);//function
console.log(fn instanceof Object);//true
但JS中的类型值并没有包括’function’,所有引用类型值都被简单的归类为object。也就是说,一个函数其实就是一个对象,既然是一个对象,它就拥有了属性和方法(函数的调用或许可以理解为它有个’调用’属性)。
问题来了,我们知道可以通过new操作符创建一个函数的实例对象:
var obj = new Fn();
我们其实可以进一步说,所有对象都是函数创建的:
var obj = {
a: 1,
b: 2
}
上述通过对象字面量创建对象的实际步骤是:1
2
3var obj = new Object(); //Object构造函数
obj.a = 1;
obj.b = 2;
可是,函数本身即是对象,它又是由谁创建的呢?
prototype属性
函数和对象密不可分,函数创建对象,自己也是对象,这种鸡生蛋,蛋生鸡的关系得捋一捋:
每个函数对象都有一个prototype
属性,它指向一个对象,称为函数原型,函数原型拥有一个constructor
属性,它又指回原来的函数对象,就像一个环:1
2
3
4
5function fn() {
...
}
console.log(typeof fn.prototype);//object
console.log(fn.prototype.constructor === fn);//true;
我们所见的所有函数都拥有函数原型,像是引用类型的构造函数:String拥有String.prototype
,Array拥有Array.prototype
,当然对象的构造函数Object的函数原型为Object.prototype
:
我们可以看见Object.prototype
中有许多我们熟知的方法:toString(),valueOf(),hasOwnProperty等,也就是说,我们平时在对象上调用的一般方法,其实来自于Object.prototype
:1
2var obj = {...};
obj.toString();
emmm也就是说,函数的原型对象可以拥有属性和方法,这也是形成原型链的重要因素;该继续解答问题了,函数本身即是对象,它又是由谁创建的呢?
[[prototype]]
每个JavaScript对象都会有一个特殊的内部属性[[prototype]]
,这个属性不能被直接访问(其实可以通过__proto__
访问),且指向着一个对象,这个对象就是上文提到的创建这个对象的函数原型,先把对象分为几类:
自定义函数的实例
1 | function Fn(a) { |
可以看到,在函数原型上添加的方法可以通过实例来访问,而通过三种不同的方法也可以证明实例的[[prototype]]
属性引用对象就是函数原型(注意通过__proto__
访问是两个下划短线__);
特殊类型的实例(Array,String,RegExp)
1 | var arr = []; |
可以看到,特殊类型其实就是系统定义的构造函数,Function
类型也是如此:1
2
3//Function自己创建自己,还创建了其他函数
console.log(Function.__proto__ === Function.prototype);
console.log(Function.prototype.__proto__ === Object.prototype);
回到最初的问题:函数本身是由谁创建的呢?答案已经很明显了,function Function
创建了所有函数包括自己;
普通对象
1 | var obj = {}; |
原型链
总之,上述例子可以理解为:
- 实例的
__proto__
为其构造函数的prototype - 构造函数的prototype的
__proto__
为Object.prototype - 构造函数的
__proto__
为Function.prototype //call,apply等方法,length,argument等属性都定义在这 - Function.prototype的
__proto__
为Object.prototype - Object.prototype的
__proto__
为null
这样沿着[[prototype]]
(或者__proto__
)逐级往上,就是原型继承中查找属性的过程,这些prototype构成的路径就被称为原型链,对于对象的[[Get]]操作会触发对原型链的查找:
- 先检查实例内部是否含有所需的属性和方法,有则返回
- 若没有,再检查实例的函数原型
- 若没有,继续检查函数原型的函数原型
- …
原型链的顶端为Object.prototype
,再往上便为null,如果在此处仍未找到所需的属性,便会返回undefined
;for..in
操作的原理和遍历原型链类似(前提是属性为enumerable),任何可以通过原型链访问的属性都会被枚举;
用in
操作符查找属性也会查找对象整条原型链;但hasOwnProperty
方法只查找实例;
总体的原型链图如下:
原型继承
只有函数对象有prototype属性,但所有对象都有[[prototype]]内部属性。
继承是面向对象语言Java、C++等的重要特点,子类可以继承父类的公有属性和方法,并重写父类方法(多态);然而。JavaScript并没有类的概念,所谓的’继承’也和Java、C++大相径庭,原本,继承意味着复制操作(一个对象复制到另一个),但JS并不会复制对象属性,而是在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数(之后再说~);
JavaScript通过原型链来实现原型继承:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function Person(name,sex) {
this.name = name;
}
Person.prototype.sayName = function() {
return this.name;
}
function Student(name,id) {
Person.call(this, name);
this.id = id;
}
//也可以使用 Student.prototype = new Person(); 但不推荐
//创建一个对象,它的__proto__链接至Person.prototype,然后其赋值给Student.prototype
Student.prototype = Object.create(Person.prototype);
//注意Student.prototype的constructor不再指向Student
//需要的话可以手动修复
Student.prototype.sayId = function() {
return this.id;
}
var ming = new Student("Ming", "390");
ming.sayName();//Ming
ming.sayId();//390
console.log(ming instanceof Person);//true
使用ES5的Object.create函数可以凭空创建一个“新对象”并把新对象的内部[[prototype]]
关联到你所指定的对象;它的polyfill代码如下:1
2
3
4
5
6
7if(!Object.create) {
Object.create = function(o) {
function F() {};
F.prototype = o;
return new F();
}
}
通过Student.prototype = new Person()
的确可以创建一个关联到Person.prototype的新对象,但是它进行了Person函数的”构造函数调用”,如果Person函数有一些副作用(给this赋值,关联到其他对象等),就会影响到Student的后代;使用Object.create(…)的唯一坏处就是创建新对象需要抛弃原有的对象,不能直接修改默认的对象;
而ES6新增的辅助函数setPrototypeOf(..)可以解决这个问题:1
2
3
4//ES6之前需要丢弃原有的Student.prototype
Student.prototype = Object.create(Person.prototype);
//ES6之后可以直接修改现有对象,所以Student.prototype修改后仍有constructor属性
Object.setPrototypeOf(Student.prototype, Person.prototype);
反射(reflect)
在传统的类环境中,检查一个实例的继承祖先通常被称为反射;在JavaScript中,找到对象的委托关联有两种方法:
从’类’的角度来看,可用
instanceof
操作符,它的左端是一个对象,右端是一个函数:它通过检查左端对象的原型链__proto__
来和右端函数的函数原型prototype
进行比较:1
2
3
4
5
6function Bar(){};
function Foo(){};
Foo.prototype = Object.create(Bar.prototype);
var a = new Foo();
//即a的原型链中是否出现了Bar.prototype?
console.log(a instanceof Bar); //true
从’对象’的角度来看,可用
isPrototypeOf(...)
方法,它更简洁明了,只需要一个可以判别的对象来调用这个方法,下面的例子回答的问题是:在a的整条原型链中是否出现过Foo.prototype?1
Foo.prototype.isPrototypeOf(a);
也可以直接获取一个对象的原型链,方法是Object.getPrototypeOf(...)
,
1
2
3//注意Object.getPrototype(obj) 不是 obj.prototype
Object.getPrototypeOf(a) === Foo.prototype; //true;
Object.getPrototypeOf(a) === Bar.prototype; //false 只能找到最近的原型
总结
初学起来有点晕,原型链的用法还有一种:行为委托,它更为安全和简洁,有空再看看~