1. 作为函数调用,指代的是全局对象
如果我们定义以下函数:
function f() {
console.log(this);
}
然后将其作为普通对象调用,即f(),此时this指代的是全局对象window。
2. 作为对象的方法调用,指代是调用对象本身
如果我们将f作为一个对象的方法,也就是说作如下改造:
var a = {};
a.f = f;
然后通过对象a调用方法f,即a.f(),此时this指代的就是调用者a。
3. 作为构造器函数使用,指代的隐含的新建对象
如果我们用new语句调用函数f,即new f(), 则此时this指代的是新建的对象。
4. call和apply的方式
除了上述几种方式外,我们还可以任意指定this的指代,这就是call和apply发挥作用的地方了。call和apply是每个函数对象都拥有的方法,其第一个参数就是要指定的this值,后面是函数正常的参数值。其细微的差别在于参见下面的示例:
function g(name, age) {
this.name = name;
this.age = age
}
var a = {};
g.call(a, 'xiaoming', 18);
g.apply(a, ['xiaoming', 18]);
即call的非this参数只需在后面列出就可以了,apply要把它们封装到一个数组里面去。
从某种程度上说,前面的三种函数调用形式都是call方式的一种语法糖:
f() === f.call(window)
a.f() === f.call(a)
new f() === var _a = {}; f.call(_a)
call有很强大的能力,我们常常使用的函数借用就是利用call可以动态指定this这一特性的。例如Array.prototype.slice.call(a)可以将看起来像数组的对象a转化为实际的数组对象。
var a={length:2,0:'first',1:'second'};
Array.prototype.slice.call(a);// ["first", "second"]
var a={length:2};
Array.prototype.slice.call(a);// [undefined, undefined]
构造函数的prototype属性和对象的__proto__隐式链接
我在这里不说原型和基于原型的面向对象模式了。诚然,JavaScript确实是一门基于原型的面向对象的语言,它确实不是一门基于类模板的面向对象语言。但在这里我不对这两种面向对象模式进行讨论来。老实说,对于思想的东西,我讨论不好。我只能列出在JavaScript已经有的东西,以及这个东西能干什么。
JavaScript确实不能定义类,但却有构造函数的概念。在关于this的讨论中也提到这一点了。对于一个普通的函数,如果通过new语句的方式调用,它就变成了构造函数了。在这里我给出一个具体的例子:
function Person(name, age) {
this.name = name;
this.age = age;
}
new Person('xiaoming', 18); //return {name: 'xiaoming', age: 18}
一般来说,构造函数会首字母大写。这会让我们产生醒目的感觉,防止我们忘记了加入new。特别注意,new Person(name, age)跟Person(name, age)的含义是截然不同的。它们中的主要区别在于this的指代不同。对于new方式,this指代的是隐含新建的对象,是我们的意愿;对于忘记new的方式,this指代的是全局window对象,此时我们在修改window对象,是非常危险的。这也是一般不推荐new语句新建对象的原因。
每个对象都有一个神秘的链接__proto__,我们可以称之为原型(原型就是这么来的,PS:我瞎说的)。原型的用途在于,如果属性在当前的对象找不到,则一律抛到原型中去找。给个具体的示例:
var a = {};
var proto = {};
proto.b = 'hello';
a.__proto__ = proto;
a.b //=> 'hello'
(注:直接操作__proto__绝不是最佳实践)
每个函数都有一个prototype属性。一般说来,只有这个函数被当成构造函数调用时才有意义。如果一个函数通过new语句调用了,新建的对象的__proto__链接指向构造函数的prototype属性。具体的示例如下:
function F() {
//这是构造函数
}
var a = new F();
a.__proto__ === F.prototype; //=> true
这其实给出了一种JavaScript实现面向对象的一种模式,尽管这种模式并不是最佳实践。但如果小心谨慎,也是一种不错的选择。
1. 在构造函数里定义对象的私有属性:
function Person(name, age) {
this.name = name;
this.age = age;
}
2. 在构造函数的prototype中定义对象的公有属性和方法:
Person.prototype.show = function() {
return this.name + ', ' + this.age;
};
3. 通过new语句创建对象:
new Person('xiaoming', 18);
JavaScript的继承模式有很多,归根结底,要么是通过原型链的方式,要么就是属性复制的方式。
原型链
之前说过,如果某个属性在对象中找不到,就会抛到原型中去继续找。实际上这个过程能够递归进行,如果在原型中仍然找不到,就会抛到原型的原型里面继续找……直到在某个原型中找到或者原型链终止为之。这个过程给了继承的一个暗示,继承的过程就是构造原型链的过程。
如果我们有一个基类Person如下:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
return 'Hello, ' + this.name;
}
现在希望新建一个Student函数,它继承自Person函数。这意味着Student对象可以调用Person原型中的方法,如sayHello;并且可以在自己的原型中定义额外的方法。
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
Student.prototype = new Person();
Student.prototype.upgrade = function() {
this.age += 1;
this.grade += 1;
}
在上面的示例中,我们定义了构造函数Student。它通过以下的步骤实现了继承:
1. 首先它有自己的私有属性name, age, grade. 其中我们借用了构造函数Person来初始化name和age属性(`Person.call(this, name, age)`),并且初始化了Student独有的属性grade。
2. 然后`Student.prototype = new Person()`这一步是实现继承的魔法。如果我们生成一个Student对象,它的原型关系如下(箭头指示原型方向):
Student() -> Person() -> Person.prototype
如果我们生成一个Student对象,即通过new Student()的方式,则它的原型是构造函数Student的prototype,在这里是new Person();而new Person()的原型自然就是Person的prototype属性了。这些是上面的箭头关系的解释了。由于原型链最后能够到达Person.prototype,所以Student对象可以调用Person在prototype上定义的方法,也就是继承了Person的方法。
```javascript
var s = new Student();
s.sayHello(); //没问题
```
3. 最后在Student的prototype属性即Person()对象上定义新方法upgrade。另外在Student的prototype可以覆盖Person的同名方法,因为在原型链上,Student.prototype比Person.prototype靠前。
```javascript
Student.prototype.sayHello = function() {
alert(Person.prototype.sayHello.call(this));
}
```
一个比较经典的例子如下,这实现了继承链。
function Shape() {}
function TwoDShape() {}
function Triangle() {}
TwoDShape.prototype = new Shape();
Triangle.prototype = new TwoDShape();
//原型链是:Triangle() -> TwoDShape() -> Shape() -> Shape.prototype
拷贝继承
JavaScript作为基于原型的面向对象语言,最直接的方式就是把父对象的属性直接拷贝到子对象中去。这个地方不便展开说了,大致就是下面这个样子。
forr(name in Person.prototype) {
Student.prototype[name] = Person.prototype[name];
}
ES5中的方式
ES5中Object对象新加入了一个方法create,可以实现继承链。如果用这种方式,上面的代码可以改写为:
//原方式为:Student.prototype = new Person();
Student.prototype = Object.create(Person.prototype);