Mmear's 😘.

JavaScript原型

字数统计: 2.1k阅读时长: 8 min
2018/03/31 Share

一切皆对象

JavaScript和面向类的语言不同,它没有类来作为对象的抽象模式,JavaScript中只有对象;也就是咱很喜欢说的’一切皆对象’,其实这句话还有待商榷,毕竟原始类型值也只是个值,其中nullundefined还比较神秘;

函数与对象的关系

JavaScript中的Function总让人产生疑惑:

1
2
3
4
5
function 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
3
var obj = new Object(); //Object构造函数
obj.a = 1;
obj.b = 2;

可是,函数本身即是对象,它又是由谁创建的呢?

prototype属性

函数和对象密不可分,函数创建对象,自己也是对象,这种鸡生蛋,蛋生鸡的关系得捋一捋:

每个函数对象都有一个prototype属性,它指向一个对象,称为函数原型,函数原型拥有一个constructor属性,它又指回原来的函数对象,就像一个环:

1
2
3
4
5
function 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
2
var obj = {...};
obj.toString();

emmm也就是说,函数的原型对象可以拥有属性和方法,这也是形成原型链的重要因素;该继续解答问题了,函数本身即是对象,它又是由谁创建的呢?

[[prototype]]

每个JavaScript对象都会有一个特殊的内部属性[[prototype]],这个属性不能被直接访问(其实可以通过__proto__访问),且指向着一个对象,这个对象就是上文提到的创建这个对象的函数原型,先把对象分为几类:

自定义函数的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Fn(a) {
this.a = a;
};
Fn.prototype.sayYes = function() {
console.log("a is " + this.a);
}
var obj = new Fn('A');
obj.sayYes(); //a is A
console.log(obj.__proto__ === Fn.prototype); //true
console.log(obj instanceof Fn); //true
console.log(Fn.prototype.isPrototypeOf(obj));//true

//Fn也是一个对象,也有对象原型
console.log(Fn.__proto__ === Function.prototype);//true
//Fn的原型也是一个对象(Object实例),也有函数原型
console.log(Fn.prototype.__proto__ === Object.prototype);//true

可以看到,在函数原型上添加的方法可以通过实例来访问,而通过三种不同的方法也可以证明实例的[[prototype]]属性引用对象就是函数原型(注意通过__proto__访问是两个下划短线__);

特殊类型的实例(Array,String,RegExp)

1
2
3
4
5
6
7
8
var arr = [];
//数组是其实是通过new Array()创建的
console.log(arr.__proto__ === Array.prototype); //true
console.log(Array.prototype.constructor === Array); //true

//Array也是一个对象,也有对应的函数原型
console.log(Array.__proto__ === Function.prototype); //true
console.log(Array.prototype.__proto__ === Object.prototype);//true

可以看到,特殊类型其实就是系统定义的构造函数,Function类型也是如此:

1
2
3
//Function自己创建自己,还创建了其他函数
console.log(Function.__proto__ === Function.prototype);
console.log(Function.prototype.__proto__ === Object.prototype);

回到最初的问题:函数本身是由谁创建的呢?答案已经很明显了,function Function创建了所有函数包括自己;

普通对象

1
2
3
4
5
6
var obj = {};
console.log(obj.__proto__ === Object.prototype); //true
console.log(Object.__proto__ === Function.prototype); //true

//原型链的顶端
console.log(Object.prototype.__proto__)//null

原型链

总之,上述例子可以理解为:

  • 实例的__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
22
function 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
7
if(!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
    6
    function 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 只能找到最近的原型

总结

初学起来有点晕,原型链的用法还有一种:行为委托,它更为安全和简洁,有空再看看~

CATALOG
  1. 1. 一切皆对象
    1. 1.1. 函数与对象的关系
  2. 2. prototype属性
  3. 3. [[prototype]]
    1. 3.1. 自定义函数的实例
    2. 3.2. 特殊类型的实例(Array,String,RegExp)
    3. 3.3. 普通对象
    4. 3.4. 原型链
  4. 4. 原型继承
    1. 4.1. 反射(reflect)
  5. 5. 总结