对象是什么

从两个层次来理解。

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

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

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

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

 

构造函数

面向对象编程的第一步,就是要生成对象。前面说过,对象是单个实物的抽象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。

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

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

var Vehicle = function () {

  this.price = 1000;

};

为了与普通函数区别,构造函数名字的第一个字母通常大写。构造函数的特点有两个。

  • 函数体内部使用了this关键字,代表了所要生成的对象实例。
  • 生成对象的时候,必须使用new命令。

 

new 命令

new命令的作用,就是执行构造函数,返回一个实例对象使用new命令时,根据需要,构造函数也可以接受参数。

var Vehicle = function (p) {

  this.price = p;

};

var v = new Vehicle(500);

new 命令的原理

使用new命令时,它后面的函数依次执行下面的步骤。

  1. 创建一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的原型,指向构造函数的prototype属性。
  3. 将这个空对象赋值给函数内部的this关键字。
  4. 开始执行构造函数内部的代码。

也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。

 

new.target

函数内部可以使用new.target属性。如果当前函数是new命令调用,new.target指向当前函数,否则为undefined。

function f() {

  console.log(new.target === f);

}

f() // false

new f() // true

使用这个属性,可以判断函数调用的时候,是否使用new命令。

function f() {

  if (!new.target) {

    throw new Error('请使用 new 命令调用!');

  }

f() // Uncaught Error: 请使用 new 命令调用!

 

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 张三.

上面代码中,对象person1是person2的模板,后者继承了前者的属性和方法。

this关键字

this关键字是一个非常重要的语法点。this可以用在构造函数之中,表示实例对象。除此之外,this还可以用在别的场合。但不管是什么场合,this都有一个共同点:它总是返回一个对象。简单说,this就是属性或方法“当前”所在的对象。

this.property

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

下面是一个实际的例子。

var person = {

  name: '张三',

  describe: function () {

    return '姓名:'+ this.name;

  }

};

person.describe()

// "姓名:张三"

使用场合

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

(1)全局环境

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

this === window // true

function f() {

  console.log(this === window);

}

f() // true

(2)构造函数

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

var Obj = function (p) {

  this.p = p;

};

(3)对象的方法

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

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

var obj ={

  foo: function () {

    console.log(this);

  }

};

obj.foo() // obj

使用注意点

避免多层 this

由于this的指向是不确定的,所以切勿在函数中包含多层的this。

避免数组处理方法中的 this,数组的map和foreach方法,允许提供一个函数作为参数。这个函数内部不应该使用this。

避免回调函数中的 this,回调函数中的this往往会改变指向,最好避免使用。

 

对象的继承

面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过“原型对象”(prototype)实现。

 

构造函数的缺点

JavaScript 通过构造函数生成新对象,可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部。构造函数内部定义了多个属性,所有实例对象都会生成构造函数里的属性,这两个属性会定义在实例对象上面。通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。这个问题的解决方法,就是 JavaScript 的原型对象(prototype)。

 

prototype 属性的作用

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

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

关于原型实例对象

  • 只要修改原型对象,变动就立刻会体现在所有实例对象上。
  • 如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。
  • 当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法

function Animal(name) {

  this.name = name;

}

Animal.prototype.color = 'white';

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

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

cat1.color // 'white'

cat2.color // 'white'

 

原型链

JavaScript 规定,所有对象都有自己的原型对象(prototype)。由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。这就是所有对象都有valueOf和toString方法的原因,因为这是从Object.prototype继承的Object.prototype的原型是null。null没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null

读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)。

 

constructor 属性

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

constructor属性定义在prototype对象上面,意味着可以被所有实例对象继承。

构造函数的实例对象自身没有constructor属性,该属性其实是读取原型链上面的P.prototype.constructor属性。

 

constructor属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。另一方面,有了constructor属性,就可以从一个实例对象新建另一个实例。

function Constr() {}

var x = new Constr();

var y = new x.constructor();

y instanceof Constr // true

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