JavaScript面向对象精要

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);

时间: 2024-09-19 09:52:14

JavaScript面向对象精要的相关文章

《JavaScript面向对象精要》——第1章 原始类型和引用类型 1.1 什么是类型

第1章 原始类型和引用类型 大多数开发者在使用Java或C#等基于类的语言的过程中学会了面向对象编程.由于JavaScript没有对类的正式支持,这些开发者在学习JavaScript时往往会迷失方向.JavaScript不需要在开头就定义好各种类,你可以在写代码的过程中根据需要创建数据结构.由于JavaScript缺少类,也就缺少用于对类进行分组的包.在Java中,包和类的名字不仅定义了对象的类型,也在工程中列出文件和目录的层次结构,JavaScript编程就好像从一块空白石板开始:你可以在上面

《JavaScript面向对象精要》——导读

**前言**JavaScript拥有上述全部特性,因为语言本身没有类的概念,所以某些特性可能不是以你所期望的方式实现的.乍一看,一个JavaScript程序可能像是一个用C来编写的面向过程的程序.如果写一个函数并传递一些参数,就有了一个看上去没有对象也可工作的脚本.但是仔细观察,你就会在点号的使用上发现对象的存在. 很多面向对象的语言使用点号来访问对象的属性和方法,JavaScript也不例外.但是你永远不需要在JavaScript中写一个类定义,导入一个包或包含一个头文件.你只是用你需要的数据

《JavaScript面向对象精要》——1.8 原始封装类型

1.8 原始封装类型 JavaScript中一个最让人困惑的部分可能就是原始封装类型的概念.原始封装类型共有3种(String.Number和Boolean).这些特殊引用类型的存在使得原始类型用起来和对象一样方便.(如果你不得不用独特的语法或切换为基于过程的编程方式来获取一个子字符串,那就太让人困惑啦). 当读取字符串.数字或布尔值时,原始封装类型将被自动创建.例如,下列代码第一行,一个原始字符串的值被赋给name.第二行代码把name当成一个对象,使用点号调用了charAt方法. var n

《JavaScript面向对象精要》——1.4 内建类型实例化

1.4 内建类型实例化 你已经见过如何用new Object()创建和使用通用对象.Object类型只是JavaScript提供的少量内建引用类型之一.其他内建类型各有它们的特殊用途,可在任何时候被实例化. 这些内建类型如下. Array 数组类型,以数字为索引的一组值的有序列表 Date 日期和时间类型 Error 运行期错误类型(还有一些更特别的错误的子类型) Function 函数类型 Object 通用对象类型 RegExp 正则表达式类型 可以用new来实例化每一个内建引用类型,如下.

《JavaScript面向对象精要》——1.3 引用类型

1.3 引用类型 引用类型是指JavaScript中的对象,同时也是你在该语言中能找到的最接近类的东西.引用值是引用类型的实例,也是对象的同义词(本章后面将用对象指代引用值).对象是属性的无序列表.属性包含键(始终是字符串)和值.如果一个属性的值是函数,它就被称为方法.JavaScript中函数其实是引用值,除了函数可以运行以外,一个包含数组的属性和一个包含函数的属性没有什么区别. 当然,在使用对象前,你必须先创建它们. 1.3.1 创建对象有时候,把JavaScript对象想象成图1-2中的哈

《JavaScript面向对象精要》——1.6 鉴别引用类型

1.6 鉴别引用类型 函数是最容易鉴别的引用类型,因为对函数使用typeof操作符时,返回值是"function". function reflect(value) { return value; } console.log(typeof reflect); // "function" 对其他引用类型的鉴别则较为棘手,因为对于所有非函数的引用类型,typeof返回"object".在处理很多不同类型的时候这帮不上什么忙.为了更方便地鉴别引用类型,

《JavaScript面向对象精要》——1.9 总结

1.9 总结 型以及未定义)的值会被直接保存在变量对象中.除了空类型,都可以用typeof来鉴别.空类型必须直接跟null进行比较才能鉴别. 引用类型是JavaScript中最接近类的东西,而对象则是引用类型的实例.可以用new操作符或字面形式创建新对象.通常可以用点号访问属性和方法,也可以用中括号.函数在JavaScript中也是对象,可以用typeof鉴别它们.至于其他引用类型,你应该用instanceof和一个构造函数来鉴别. 为了让原始类型看上去更像引用类型,JavaScript提供了3

《JavaScript面向对象精要》——1.5 访问属性

1.5 访问属性 属性是对象中保存的名字和值的配对.点号是JavaScript中访问属性的最通用做法(就跟许多面向对象语言一样),不过也可以用中括号访问JavaScript对象的属性. 例如,下面的代码使用点号. var array = []; array.push(12345); 也可以如下例用中括号,方法的名字现在由中括号中的字符串表示. var array = []; array["push"](12345); 在需要动态决定访问哪个属性时,这个语法特别有用.例如下例的中括号允许

《JavaScript面向对象精要》——1.2 原始类型

1.2 原始类型 原始类型代表照原样保存的一些简单数据,如true和25.JavaScript共有5种原始类型,如下. Boolean 布尔,值为true或false Number 数字,值为任何整型或浮点数值 String 字符串,值为由单引号或双引号括出的单个字符或连续字符(JavaScript不区分字符类型) Null 空类型,该原始类型仅有一个值:null Undefined 未定义,该原始类型仅有一个值:undefined(undefined会被赋给一个还没有初始化的变量) 前3种类型