---出自 《node.js开发指南》
提起面向对象的程序设计语言,立刻让人想起的是C++、Java 等这类静态强类型语言,以及Python、Ruby 等脚本语言,它们共有的特点是基于类的面向对象。而说到JavaScript,
很少能让人想到它面向对象的特性,甚至有人说它不是面向对象的语言,因为它没有类。没错,JavaScript 真的没有类,但JavaScript 是面向对象的语言。JavaScript 只有对象,对象就是对象,不是类的实例。
因为绝大多数面向对象语言中的对象都是基于类的,所以经常有人混淆类的实例与对象的概念。对象就是类的实例,这在大多数语言中都没错,但在JavaScript 中却不适用。
JavaScript 中的对象是基于原型的,因此很多人在初学JavaScript 对象时感到无比困惑。通过这一节,我们将重新认识JavaScript 中对象,充分理解基于原型的面向对象的实质。
构造函数
function User(name, uri) { this.name = name; this.uri = uri; this.display = function () { console.log(this.name); } } var someuser = new User(‘byvoid‘, ‘http://www.byvoid.com‘);
上下文对象
在JavaScript 中,上下文对象就是 this 指针,即被调用函数所处的环境。上下文对象的作用是在一个函数内部引用调用它的对象本身,JavaScript 的任何函数都是被某个对象调用的,包括全局对象,所以 this 指针是一个非常重要的东西。
this 指针永远是这个引用所属的对象
var someuser = { name: ‘byvoid‘, func: function () { console.log(this.name); } }; var foo = { name: ‘foobar‘ }; someuser.func(); // 输出byvoid foo.func = someuser.func; foo.func(); // 输出foobar name = ‘global‘; func = someuser.func; func(); // 输出global
call 和 apply
call 和 apply 的功能是以不同的对象作为上下文来调用某个函数。简而言之,就是允许一个对象去调用另一个对象的成员函数
call 和 apply 的功能是一致的,两者细微的差别在于 call 以参数表来接受被调用函数的参数,而 apply 以数组来接受被调用函数的参数。call 和 apply 的语法分别是:
func.call(thisArg[, arg1[, arg2[, ...]]])
func.apply(thisArg[, argsArray])
其中,func 是函数的引用,thisArg 是 func 被调用时的上下文对象,arg1、arg2 或argsArray 是传入 func 的参数
var someuser = { name: ‘byvoid‘, display: function (words) { console.log(this.name + ‘ says ‘ + words); } }; var foo = { name: ‘foobar‘ }; someuser.display.call(foo, ‘hello‘); // 输出foobar says hello
someuser.display 是被调用的函数,它通过 call 将上下文改变为 foo 对象,因此在函数体内访问 this.name 时,实际上访问的是foo.name,因而输出了foobar。
bind
可以用 call 或 apply 方法,但如果重复使用会不方便,因为每次都要把上下文对象作为参数传递,而且还会使代码变得不直观。针对这种情况,我们可以使用 bind 方法来永久地绑定函数的上下文,使其无论被谁调用,上下文都是固定的。bind 语法如下:
func.bind(thisArg[, arg1[, arg2[, ...]]])
bind 方法还有一个重要的功能:绑定参数表,如下例所示。
var person = { name: ‘byvoid‘, says: function (act, obj) { console.log(this.name + ‘ ‘ + act + ‘ ‘ + obj); } }; person.says(‘loves‘, ‘diovyb‘); // 输出byvoid loves diovyb byvoidLoves = person.says.bind(person, ‘loves‘); byvoidLoves(‘you‘); // 输出byvoid loves you
可以看到,byvoidLoves 将 this 指针绑定到了person,并将第一个参数绑定到loves,之后在调用 byvoidLoves 的时候,只需传入第三个参数。这个特性可以用于创建一个函数的“捷径”,之后我们可以通过这个“捷径”调用,以便在代码多处调用时省略重复输入相同的参数。
原型与构造函数
? 构造函数内定义的属性继承方式与原型不同,子对象需要显式调用父对象才能继承构造函数内定义的属性。
? 构造函数内定义的任何属性,包括函数在内都会被重复创建,同一个构造函数产生的两个对象不共享实例。
? 构造函数内定义的函数有运行时闭包的开销,因为构造函数内的局部变量对其中定义的函数来说也是可见的
//下面这段代码可以验证以上问题: function Foo() { varinnerVar = ‘hello‘; this.prop1 = ‘BYVoid‘; this.func1 = function () { innerVar = ‘‘; }; } Foo.prototype.prop2 = ‘Carbo‘; Foo.prototype.func2 = function () { console.log(this.prop2); }; var foo1 = new Foo(); var foo2 = new Foo(); console.log(foo1.func1 == foo2.func1); // 输出false console.log(foo1.func2 == foo2.func2); // 输出true
? 除非必须用构造函数闭包,否则尽量用原型定义成员函数,因为这样可以减少开销。
? 尽量在构造函数内定义一般成员,尤其是对象或数组,因为用原型定义的成员是多个实例共享的。
原型链
JavaScript 中有两个特殊的对象:Object 与Function,它们都是构造函数,用于生成对象。
Object.prototype 是所有对象的祖先,Function.prototype 是所有函数的原型,包括构造函数。
我把JavaScript 中的对象分为三类,一类是用户创建的对象,一类是构造函数对象,一类是原型对象。
用户创建的对象,即一般意义上用new 语句显式构造的对象。
构造函数对象指的是普通的构造函数,即通过 new 调用生成普通对象的函数。
原型对象特指构造函数prototype 属性指向的对象。
这三类对象中每一类都有一个__proto__ 属性,它指向该对象的原型,从任何对象沿着它开始遍历都可以追溯到 Object.prototype。
构造函数对象有prototype 属性,指向一个原型对象,通过该构造函数创建对象时,被创建对象的 __proto__ 属性将会指向构造函数的 prototype 属性。原型对象有constructor属性,指向它对应的构造函数。
让我们通过下面这个例子来理解原型:
function Foo() { } Object.prototype.name = ‘My Object‘; Foo.prototype.name = ‘Bar‘; var obj = new Object(); var foo = new Foo(); console.log(obj.name); // 输出My Object console.log(foo.name); // 输出Bar console.log(foo.__proto__.name); // 输出Bar console.log(foo.__proto__.__proto__.name); // 输出My Object console.log(foo.__proto__.constructor.prototype.name); // 输出Bar
在JavaScript 中,继承是依靠一套叫做原型链(prototype chain)的机制实现的。属性继承的本质就是一个对象可以访问到它的原型链上任何一个原型对象的属性。例如上例的foo 对象,它拥有foo. __proto__ 和 foo. __proto__.__proto__ 所有属性的浅拷贝(只复制基本数据类型,不复制对象)。所以可以直接访问foo.constructor(来自foo.__proto__,即Foo.prototype),foo.toString(来自foo. __proto__.__proto__,即Object.prototype)。