构造函数与 new 命令

对象是什么

(1)对象是单个实物的抽象。

 

一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。

 

(2)对象是一个容器,封装了属性(property)和方法(method)。

 

属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。

 

 

 

 

构造函数

所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。

 

JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。

 

构造函数就是一个普通的函数,但是有自己的特征和用法。

 

var Vehicle = function () {

  this.price = 1000;

};

 

 

 

new 命令

基本用法

new命令的作用,就是执行构造函数,返回一个实例对象。

 

var Vehicle = function () {

  this.price = 1000;

};

 

var v = new Vehicle();

v.price // 1000

 

 

 

 

Object.create() 创建实例对象

构造函数作为模板,可以生成实例对象。但是,有时拿不到构造函数,只能拿到一个现有的对象。我们希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用Object.create()方法。

 

var person1 = {

  name: '张三',

  age: 38,

  greeting: function() {

    console.log('Hi! I\'m ' + this.name + '.');

  }

};

 

var person2 = Object.create(person1);

 

person2.name // 张三

person2.greeting() // Hi! I'm 张三.

 

this 关键字

 

 

简单说,this就是属性或方法“当前”所在的对象。

 

this.property

上面代码中,this就代表property属性当前所在的对象。

 

下面是一个实际的例子。

 

var person = {

  name: '张三',

  describe: function () {

    return '姓名:'+ this.name;

  }

};

 

person.describe()

// "姓名:张三"

上面代码中,this.name表示name属性所在的那个对象。由于this.name是在describe方法中调用,而describe方法所在的当前对象是person,因此this指向person,this.name就是person.name。

 

使用场合

 

this主要有以下几个使用场合。

 

(1)全局环境

 

全局环境使用this,它指的就是顶层对象window。

 

this === window // true

 

function f() {

  console.log(this === window);

}

f() // true

上面代码说明,不管是不是在函数内部,只要是在全局环境下运行,this就是指顶层对象window。

 

(2)构造函数

 

构造函数中的this,指的是实例对象。

 

var Obj = function (p) {

  this.p = p;

};

上面代码定义了一个构造函数Obj。由于this指向实例对象,所以在构造函数内部定义this.p,就相当于定义实例对象有一个p属性。

 

var o = new Obj('Hello World!');

o.p // "Hello World!"

(3)对象的方法

 

如果对象的方法里面包含this,this的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this的指向。

 

但是,这条规则很不容易把握。请看下面的代码。

 

var obj ={

  foo: function () {

    console.log(this);

  }

};

 

obj.foo() // obj

上面代码中,obj.foo方法执行时,它内部的this指向obj。

 

 

绑定 this 的方法

Function.prototype.call()

 

函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。

 

var obj = {};

 

var f = function () {

  return this;

};

 

f() === window // true

f.call(obj) === obj // true

上面代码中,全局环境运行函数f时,this指向全局环境(浏览器为window对象);call方法可以改变this的指向,指定this指向对象obj,然后在对象obj的作用域中运行函数f。

 

 

Function.prototype.apply()

 

apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。

 

func.apply(thisValue, [arg1, arg2, ...])

 

apply方法的第一个参数也是this所要指向的那个对象,如果设为null或undefined,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加。

 

function f(x, y){

  console.log(x + y);

}

 

f.call(null, 1, 1) // 2

f.apply(null, [1, 1]) // 2

上面代码中,f函数本来接受两个参数,使用apply方法以后,就变成可以接受一个数组作为参数。

 

 

Function.prototype.bind()

bind方法用于将函数体内的this绑定到某个对象,然后返回一个新函数。

 

var d = new Date();

d.getTime() // 1481869925657

 

var print = d.getTime;

print() // Uncaught TypeError: this is not a Date object.

上面代码中,我们将d.getTime方法赋给变量print,然后调用print就报错了。这是因为getTime方法内部的this,绑定Date对象的实例,赋给变量print以后,内部的this已经不指向Date对象的实例了。

 

bind方法可以解决这个问题。

 

var print = d.getTime.bind(d);

print() // 1481869925657

上面代码中,bind方法将getTime方法内部的this绑定到d对象,这时就可以安全地将这个方法赋值给其他变量了。

 

 

 

prototype 对象

原型对象概述

构造函数的缺点

JavaScript 通过构造函数生成新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部。

 

function Cat (name, color) {

  this.name = name;

  this.color = color;

}

 

var cat1 = new Cat('大毛', '白色');

 

cat1.name // '大毛'

cat1.color // '白色'

 

 

通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。

 

function Cat(name, color) {

  this.name = name;

  this.color = color;

  this.meow = function () {

    console.log('喵喵');

  };

}

 

var cat1 = new Cat('大毛', '白色');

var cat2 = new Cat('二毛', '黑色');

 

cat1.meow === cat2.meow

// false

 

每新建一个实例,就会新建一个meow方法。这既没有必要,又浪费系统资源,因为所有meow方法都是同样的行为,完全应该共享。

 

这个问题的解决方法,就是 JavaScript 的原型对象(prototype)。

 

 

prototype 属性的作用

JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。

 

 

对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。

 

function Animal(name) {

  this.name = name;

}

Animal.prototype.color = 'white';

 

var cat1 = new Animal('大毛');

var cat2 = new Animal('二毛');

 

cat1.color // 'white'

cat2.color // 'white'

上面代码中,构造函数Animal的prototype属性,就是实例对象cat1和cat2的原型对象。原型对象上添加一个color属性,结果,实例对象都共享了该属性。

 

总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象。

 

Animal.prototype.walk = function () {

  console.log(this.name + ' is walking');

};

上面代码中,Animal.prototype对象上面定义了一个walk方法,这个方法将可以在所有Animal实例对象上面调用。

 

 

constructor 属性

 

prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。

 

function P() {}

P.prototype.constructor === P // true

 

 

constructor属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。

 

function F() {};

var f = new F();

 

f.constructor === F // true

f.constructor === RegExp // false

上面代码中,constructor属性确定了实例对象f的构造函数是F,而不是RegExp。

 

 

instanceof 运算符

instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。

 

var v = new Vehicle();

v instanceof Vehicle // true

上面代码中,对象v是构造函数Vehicle的实例,所以返回true。

 

 

 

instanceof运算符的左边是实例对象,右边是构造函数

 

 

Object 对象的相关方法

Object.getPrototypeOf()

Object.getPrototypeOf方法返回参数对象的原型。这是获取原型对象的标准方法。

 

var F = function () {};

var f = new F();

Object.getPrototypeOf(f) === F.prototype // true

上面代码中,实例对象f的原型是F.prototype。

 

Object.setPrototypeOf()

 

Object.setPrototypeOf方法为参数对象设置原型,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象。

 

var a = {};

var b = {x: 1};

Object.setPrototypeOf(a, b);

 

Object.getPrototypeOf(a) === b

a.x // 1

上面代码中,Object.setPrototypeOf方法将对象a的原型,设置为对象b,因此a可以共享b的属性。

 

 

Object.create()

JavaScript 提供了Object.create方法,用来满足这种需求。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性。

 

// 原型对象

var A = {

  print: function () {

    console.log('hello');

  }

};

 

// 实例对象

var B = Object.create(A);

 

Object.getPrototypeOf(B) === A // true

B.print() // hello

B.print === A.print // true

上面代码中,Object.create方法以A对象为原型,生成了B对象。B继承了A的所有属性和方法。

 

 

下面三种方式生成的新对象是等价的。

 

var obj1 = Object.create({});

var obj2 = Object.create(Object.prototype);

var obj3 = new Object();

 

 

 

除了对象的原型,Object.create方法还可以接受第二个参数。该参数是一个属性描述对象,它所描述的对象属性,会添加到实例对象,作为该对象自身的属性。

 

var obj = Object.create({}, {

  p1: {

    value: 123,

    enumerable: true,

    configurable: true,

    writable: true,

  },

  p2: {

    value: 'abc',

    enumerable: true,

    configurable: true,

    writable: true,

  }

});

 

// 等同于

var obj = Object.create({});

obj.p1 = 123;

obj.p2 = 'abc';

 

Object.prototype.isPrototypeOf()

 

 

实例对象的isPrototypeOf方法,用来判断该对象是否为参数对象的原型。

 

var o1 = {};

var o2 = Object.create(o1);

var o3 = Object.create(o2);

 

o2.isPrototypeOf(o3) // true

o1.isPrototypeOf(o3) // true

上面代码中,o1和o2都是o3的原型。这表明只要实例对象处在参数对象的原型链上,isPrototypeOf方法都返回true。

 

 

获取原型对象方法的比较

如前所述,__proto__属性指向当前对象的原型对象,即构造函数的prototype属性。

 

var obj = new Object();

 

obj.__proto__ === Object.prototype

// true

obj.__proto__ === obj.constructor.prototype

// true

上面代码首先新建了一个对象obj,它的__proto__属性,指向构造函数(Object或obj.constructor)的prototype属性。

 

Object.getOwnPropertyNames()

Object.getOwnPropertyNames方法返回一个数组,成员是参数对象本身的所有属性的键名,不包含继承的属性键名。

 

Object.getOwnPropertyNames(Date)

// ["parse", "arguments", "UTC", "caller", "name", "prototype", "now", "length"]

上面代码中,Object.getOwnPropertyNames方法返回Date所有自身的属性名。

 

Object.prototype.hasOwnProperty()

对象实例的hasOwnProperty方法返回一个布尔值,用于判断某个属性定义在对象自身,还是定义在原型链上。

 

Date.hasOwnProperty('length') // true

Date.hasOwnProperty('toString') // false

上面代码表明,Date.length(构造函数Date可以接受多少个参数)是Date自身的属性,Date.toString是继承的属性。

 

 

面向对象编程的模式

构造函数的继承

让一个构造函数继承另一个构造函数,是非常常见的需求。

 

这可以分成两步实现。第一步是在子类的构造函数中,调用父类的构造函数。

 

function Sub(value) {

  Super.call(this);

  this.prop = value;

}

上面代码中,Sub是子类的构造函数,this是子类的实例。在实例上调用父类的构造函数Super,就会让子类实例具有父类实例的属性。

 

第二步,是让子类的原型指向父类的原型,这样子类就可以继承父类原型。

 

Sub.prototype = Object.create(Super.prototype);

Sub.prototype.constructor = Sub;

Sub.prototype.method = '...';

上面代码中,Sub.prototype是子类的原型,要将它赋值为Object.create(Super.prototype),而不是直接等于Super.prototype。否则后面两行对Sub.prototype的操作,会连父类的原型Super.prototype一起修改掉。

 

 

 

模块

JavaScript模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。

基本的实现方法

模块是实现特定功能的一组属性和方法的封装。

function m1() {

  //...

}

 

function m2() {

  //...

}

 

 

var module1 = new Object({

 _count : 0,

 m1 : function (){

  //...

 },

 m2 : function (){

   //...

 }

});

上面的函数m1和m2,都封装在module1对象里。使用的时候,就是调用这个对象的属性。

 

module1.m1();

 

 

封装私有变量:构造函数的写法

我们可以利用构造函数,封装私有变量。

 

function StringBuilder() {

  var buffer = [];

 

  this.add = function (str) {

     buffer.push(str);

  };

 

  this.toString = function () {

    return buffer.join('');

  };

 

}

这种方法将私有变量封装在构造函数中,违反了构造函数与实例对象相分离的原则。并且,非常耗费内存。

 

function StringBuilder() {

  this._buffer = [];

}

 

StringBuilder.prototype = {

  constructor: StringBuilder,

  add: function (str) {

    this._buffer.push(str);

  },

  toString: function () {

    return this._buffer.join('');

  }

};

这种方法将私有变量放入实例对象中,好处是看上去更自然,但是它的私有变量可以从外部读写,不是很安全。

posted on 2018-02-24 11:15  Sharpest  阅读(126)  评论(0)    收藏  举报