2019-05-07 OOP总结

[20190507]

  • 实例对象与 new 命令
  • this 关键字
  • 对象的继承
  • Object 对象的相关方法
  • 严格模式

面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。

典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)

JavaScript 语言使用构造函数(constructor)作为对象的模板。

构造函数的名字第一个字母大写。

// 构造函数
var Vehicle = function() {
    this.price = 1000;
}
var v = new Vehicle();
v.price // 1000

构造函数的特点有两个。

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

使用new命令时,根据需要,构造函数也可以接受参数。

var Vehicle = function(p) {
    this.price = p;
}
var v = new Vehicle(500);

不用new的话,直接调用构造函数的话,等同于直接调普通函数。

一个很自然的问题是,如果忘了使用new命令,直接调用构造函数会发生什么事?

这种情况下,构造函数就变成了普通函数,并不会生成实例对象。而且由于后面会说到的原因,this这时代表全局对象,将造成一些意想不到的结果。

var Vehicle = function (){
  this.price = 1000;
};

var v = Vehicle();
v // undefined
price // 1000

严格模式下,可以显示提醒这是个错误用法。

function Fubar(foo, bar){
  'use strict';
  this._foo = foo;
  this._bar = bar;
}

Fubar()
// TypeError: Cannot set property '_foo' of undefined

严格模式下,函数内部的this不能指向全局对象。

更健壮的写法是:

function Fubar(foo, bar) {
    if !(this instanceof Fubar) {
        return new Fubar(foo, bar);
    }
    this._foo = foo;
    this._bar = bar;
}

Fubar(1, 2)._foo // 1
(new Fubar(1, 2))._foo // 1

执行new的过程:

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

this指向新生成的空对象,针对this的操作,都会发生在这个空对象上。

构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。

如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。

var Vehicle = function () {
  this.price = 1000;
  return 1000;
};

(new Vehicle()) === 1000
// false

上面代码中,构造函数Vehicle的return语句返回一个数值。这时,new命令就会忽略这个return语句,返回“构造”后的this对象。

但是,如果return语句返回的是一个跟this无关的新对象,new命令会返回这个新对象,而不是this对象。这一点需要特别引起注意。

var Vehicle = function (){
  this.price = 1000;
  return { price: 2000 };
};

(new Vehicle()).price
// 2000

new.target属性

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

function f() {
  console.log(new.target === f);
}

f() // false
new f() // true

或者:

function f() {
  if (!new.target) {
    throw new Error('请使用 new 命令调用!');
  }
  // ...
}

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

以现有对象为模板生成新的对象: 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用在构造函数中,表示实例对象。

不管this用在什么场合,共同点都是:返回一个对象。

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

var person = {
    name: "BING",
    describe: function () {
        return "name: " + this.name;
    }
}

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

this指向的可变性

由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即this的指向是可变的。

var A = {
  name: '张三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

var B = {
  name: '李四'
};

B.describe = A.describe;
B.describe()
// "姓名:李四"

A.describe() // "姓名:张三"

上面代码中,A.describe属性被赋给B,于是B.describe就表示describe方法所在的当前对象是B,所以this.name就指向B.name。

更清晰的表示:

function f() {
    return "姓名:" + this.name;
}

var A = {
    name: "张三",
    describe: f
}

var A = {
    name: "李四",
    describe: f
}

A.describe() // "姓名:张三"
B.describe() // "姓名:李四"

随着f所在的对象不同,this的指向也发生变化。

只要函数被赋给另一个变量,this的指向就会变。

下面这个例子更有趣:

var A = {
  name: '张三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

var name = '李四';
var f = A.describe;
f() // "姓名:李四"

上面代码中,A.describe被赋值给变量f,内部的this就会指向f运行时所在的对象(本例是顶层对象)。

网页编程示例:




上面代码是一个文本输入框,每当用户输入一个值,就会调用onChange回调函数,验证这个值是否在指定范围。浏览器会向回调函数传入当前对象,因此this就代表传入当前对象(即文本框),然后就可以从this.value上面读到用户的输入值。

总结

JS中一切皆对象,运行环境也是对象,函数是在某个对象中运行,this就是函数运行时所在的对象。

这个含义是明确的,但是因为JS本身支持环境的动态切换,所以this的指向是动态的,不能事先确定到底指向哪个对象。

this的实质

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。

this的使用场合

  • 全局环境
  • 构造函数
  • 对象的方法

分类上来说就三个类别。

全局环境

this === window // true

function f() {
  console.log(this === window);
}
f() // true

构造函数

var Obj = function(p) {
    this.p = p;
} 
var o = new Obj("hello world!");
o.p; // "hello world!"

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

对象的方法

如果对象的方法中包含this,则this指向的就是方法运行时所在的对象。

将该方法赋值给另一个对象,就会改变this的指向。

var obj ={
  foo: function () {
    console.log(this);
  }
};

obj.foo() // obj

下面三种都改变了this的指向:

// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window

上面代码中,obj.foo就是一个值。这个值真正调用的时候,运行环境已经不是obj了,而是全局环境,所以this不再指向obj。

可以这样理解,JavaScript 引擎内部,obj和obj.foo储存在两个内存地址,称为地址一和地址二。obj.foo()这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this指向全局环境。上面三种情况等同于下面的代码。

// 情况一
(obj.foo = function () {
  console.log(this);
})()
// 等同于
(function () {
  console.log(this);
})()

// 情况二
(false || function () {
  console.log(this);
})()

// 情况三
(1, function () {
  console.log(this);
})()

this会指向最靠近的那个对象:

var a = {
  p: 'Hello',
  b: {
    m: function() {
      console.log(this.p);
    }
  }
};

a.b.m() // undefined

改成下面这样即可:

var a = {
  b: {
    m: function() {
      console.log(this.p);
    },
    p: 'Hello'
  }
};

嵌套对象内部的方法赋值给一个变量,会使得this指向发生转移:

var a = {
  b: {
    m: function() {
      console.log(this.p);
    },
    p: 'Hello'
  }
};

var hello = a.b.m;
hello() // undefined

将嵌套对象内部的方法赋值给一个变量,this依然会指向全局对象。

避免多层this

var o = {
  f1: function () {
    console.log(this);
    var f2 = function () {
      console.log(this);
    }();
  }
}

o.f1()
// Object
// Window

上面代码包含两层this,结果运行后,第一层指向对象o,第二层指向全局对象,因为实际执行的是下面的代码。

var temp = function () {
  console.log(this);
};

var o = {
  f1: function () {
    console.log(this);
    var f2 = temp();
  }
}

this的传递:

var o = {
  f1: function() {
    console.log(this);
    var that = this;
    var f2 = function() {
      console.log(that);
    }();
  }
}

o.f1()
// Object
// Object

上面代码定义了变量that,固定指向外层的this,然后在内层使用that,就不会发生this指向的改变。

事实上,使用一个变量固定this的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握。

内层this不指向外部,而是指向了顶层对象。

避免回调函数中的this

var o = new Object();
o.f = function () {
  console.log(this === o);
}

// jQuery 的写法
$('#button').on('click', o.f);

上面代码中,点击按钮以后,控制台会显示false。原因是此时this不再指向o对象,而是指向按钮的 DOM 对象,因为f方法是在按钮对象的环境中被调用的。这种细微的差别,很容易在编程中忽视,导致难以察觉的错误。

为了解决这个问题,可以采用下面的一些方法对this进行绑定,也就是使得this固定指向某个对象,减少不确定性。

绑定 this

this的动态切换,为JS提供了巨大的灵活性,有时需要把this固定下来。
三种固定方法:

  • call
  • apply
  • bind

call()

var obj = {};

var f = function () {
  return this;
};

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

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

注意是在函数实例上调用call

function add(a, b) {
  return a + b;
}

add.call(this, 1, 2) // 3

call方法还可以接受多个参数,call的第一个参数就是this所要指向的那个对象,后面的参数则是函数调用时所需的参数。

apply()

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

func.apply(thisValue, [arg1, arg2, ...])
function f(x, y){
  console.log(x + y);
}

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

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

求数组的最大元素

var a = [10, 2, 4, 15, 9]
Math.max.apply(null, a);

bind()

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

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

bind方法的参数就是所要绑定this的对象,下面是一个更清晰的例子。

var counter = {
  count: 0,
  inc: function () {
    this.count++;
  }
};

var func = counter.inc.bind(counter);
func();
counter.count // 1

参考

https://wangdoc.com/javascript/oop/new.html
https://wangdoc.com/javascript/oop/this.html

你可能感兴趣的:(2019-05-07 OOP总结)