Skip to content

面向对象

面向对象

面向对象编程将系统抽象为许多对象的集合,每一个对象代表了这个系统的特定方面。

对象是什么?

  • 对象是单个实物的抽象

当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现时情况,针对对象进行编程

  • 对象是一个容器,封装了属性property和方法method

属性是对象的状态,方法是对象的行为(完成某种任务)

类和实例

类并不做任何事情,只是一种用于创建具体对象的模板。

构造函数、属性、方法

具体传值的是实例

继承

Person类是Professor类和Student类的超类或父类, Professor类和Student类是Person类的子类。

当一个方法拥有相同的函数名,但在不同类中可以具有不同的实现时,称这一特性为多态

当一个方法在子类中替换了父类的实现时,称之为子类重写/重载了父类中的实现

封装

保持对象内部状态的私有性、明确划分对象的公共接口和内部状态,这些特性称之为封装

面向对象与js

js的核心特性:

  • 构造函数:基于类的面向对象编程中,类和对象是两个不同的概念,对象通常是类创造的实例,定义类的方式和实例化对象的方式不同。js中,会使用函数或对象字面量创建对象。js可以在没有特定的类定义的情况下创建对象,相对于基于类的面向对象编程,这种方式更轻量

  • 原型链:很自然的实现了继承特性。但与继承仍有区别。继承是当一个子类完成继承时,由该子类所创建的对象既具有子类中单独定义的属性,又具有父类中定义的属性;原型链中,每个层级都代表了一个不同的对象,不同的对象之间通过__proto__属性链接起。原型链的行为更像是委派。委派同样是对象中的一种编程模式。当我们要求对象执行某项任务时,在委派模式下,对象可以自己执行该项任务,或者要求另一个对象(委派的对象)以其自己的方式执行这项任务。在许多方面,相对于继承来说,委派可以更为灵活地在许多对象之间建立联系(例如,委派模式可以在程序运行时改变、甚至完全替换委派对象)。

直接使用构造函数和原型去实现这些特性(例如继承)是棘手的,因此,JavaScript 提供了一些额外的特性,这些特性在原型这一模型之上又抽象出一层模型,将基于类的面向对象编程中的概念映射到原型中,从而能够更为直接地在 JavaScript 中使用基于类的面向对象编程中的概念。

构造函数

JavaScript对象体系,不是基于“类”,而是基于“构造函数constructor”和“原型链prototype”

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

所谓构造函数,就是专门用来生成实例对象的函数,他就是对象的模板,描述实例对象的基本结构,一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。

构造函数就是一个普通的函数,但具有自己的特征和用法。为了与普通函数区别,构造函数名字的第一个字母通常大写。

构造函数的特点:

  • 函数体内部使用this关键字,代表了所要生成的对象实例

  • 生成对象的时候,必须使用new命令

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

new命令

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

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

var v = new Vehicle(500);

new命令本身就可以执行构造函数,也可以不带括号,但不推荐

js
// 推荐的写法
var v = new Vehicle();
// 不推荐的写法
var v = new Vehicle;

如果忘记使用new命令,直接调用构造函数会发生什么事?

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

为了保证构造函数必须与new命令一起使用,

  • 一个解决方法是,构造函数内部使用严格模式,即第一行加上use strict。这样,一旦忘记使用new命令,直接调用构造函数就会报错。
js
function Fubar(foo, bar){
  'use strict';
  this._foo = foo;
  this._bar = bar;
}

Fubar()
// TypeError: Cannot set property '_foo' of undefined
  • 另一个解决方法,构造函数内部判断是否使用new命令,如果发现没有使用,则直接返回一个实例对象
js
function Fubar(foo, bar){
  if(!(this instanceof Fubar)){
    return new Fubar(foo,bar);
  }

  this._foo=foo;
  this._bar=bar;
}

new命令的原理

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

1、创建一个空对象,作为将要返回的对象实例;

2、将这个空对象的原型,指向构造函数的prototype属性;

3、将这个空对象赋值给函数内部的this关键字;

4、开始执行构造函数内部的代码。

INFO

也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在空对象上。

构造函数,目的就是,操作一个空对象(即this对象),将其“构造”成需要的样子

js
function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params){
  // 将arguments对象转为数组
  var args = [].slice.call(arguments);
  // 取出构造函数
  var constructor = args.shift();
  // 创建一个空对象,继承构造函数的prototype属性
  var context = Object.create(constructor.prototype);
  // 执行构造函数
  var result = constructor.apply(context, args);
  // 如果返回结果是对象,就直接返回,否则返回context对象
  return (typeof result === 'object' && result != null) ? result : context;
}

// 实例
var actor = _new(Person, '张三', 28);

new.target 函数内部可以使用new.target属性

如果当前函数是new命令调用,new.target指向当前函数,否则为undefined

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

js
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 命令调用!

this关键字

  • this可以用在构造函数之中,表示实例对象,除此之外,this还可以用在别的场合。但不管什么场合,this都有一个共同点:它总是返回一个对象。

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

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

person.describe()
// "姓名:张三"
  • 由于对象的属性可以赋值给另一个对象,所以属性所在的当前对象是可变的,即this的指向是可变的
js
var A={
  name:'张三',
  describe:function(){
    return '姓名:'+this.name;
  }
};

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

B.describe = A.describe;
B.describe();
//姓名:李四
js
function f() {
  return '姓名:'+ this.name;
}

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

var B = {
  name: '李四',
  describe: f
};

A.describe() // "姓名:张三"
B.describe() // "姓名:李四"
  • 只要函数被赋给另一个变量,this的指向就会变
js
var A = {
  name: '张三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

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

this的实质

this的设计,跟内存里面的数据结构有关

js
var obj = { foo:  5 };

obj是一个地址,读取foo会先从obj拿到内存地址,然后从该地址读取原始对象,返回foo属性

原始对象以字典结构保存,每一个属性名都对应一个属性描述对象

js
{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

// foo属性的指保存在属性描述对象的value属性里面
js
var obj = { foo: function () {} };

如果属性是函数,引擎会将函数单独保存在内存中,然后将函数的地址赋值给foo属性的value属性。

js
{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行

js
var f=function(){};

var obj={f:f};

// 单独执行
f();

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

javascript运行在函数内部,引用当前函数的其他变量

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

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

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

this的使用场合

1、全局变量:指的是顶层对象window

2、构造函数:指的是实例对象

3、对象的方法:方法里包含this就指方法运行时所在的对象,赋值给其他对象,就会改变this的指向

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

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

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

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

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

如果this所在的方法不在对象的第一层,这时this只是指向当前一层的对象,而不会继承更上面的层。

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

a.b.m() // undefined

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

var a = {
  p: 'Hello',
  b: b
};

(a.b).m() // 等同于 b.m()

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

注意点

1、避免多层this

2、避免数组处理方法的this

数组的map和forEach方法,允许提供函数作为参数,函数内部不应该使用this,原因也是多层this,内层的this不指向外部,而指向顶层对象

js
var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    });
  }
}

o.f()
// undefined a1
// undefined a2

解决办法:1、使用中间变量固定this;2、将this当作forEach的第二个参数,固定运行环境。

js
var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    var that = this;
    this.p.forEach(function (item) {
      console.log(that.v+' '+item);
    });
  }
}

o.f()
// hello a1
// hello a2

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    }, this);
  }
}

o.f()
// hello a1
// hello a2

3、避免回调函数中的this

回调函数中的this往往会改变this指向

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

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

绑定this的方法

1、Function.prototype.call()

js
var obj = {};

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

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

call方法的参数,是一个对象。如果为nullundefined,则默认传入全局对象。

接收多个参数,第一个是this要指向的那个对象,后面的参数是函数调用时所需的参数

js
func.call(thisValue, arg1, arg2, ...)

2、Function.prototype.apply()

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

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

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

Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]

Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined]
Array.prototype.slice.apply({length: 1}) // [undefined]

3、Function.prototype.bind()

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

js
var d = new Date();
d.getTime() // 1481869925657

var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.

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

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

js中的类

ES6引入了Class(类)这个概念。作为对象的模板。通过class关键字,可以定义类。

  • ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更清晰,更像面向对象编程的语法而已。

  • ES6的类,完全可以看作构造函数的另一种写法。

js
class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true
  • 使用也是直接对类使用new命令,跟构造函数的用法完全一致。

  • 构造函数的prototype属性,在ES6的类上继续存在。类的所有方法都定义在类的prototype属性上面

js
class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同于

Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

类和构造函数

js
class Person {
  name;

  constructor(name) {
    this.name = name;
  }

  introduceSelf() {
    console.log(`Hi! I'm ${this.name}`);
  }
}

const giles = new Person("Giles");

giles.introduceSelf(); // Hi! I'm Giles

构造函数使用constructor关键字来声明:

  • 创建一个新的对象

  • 将this绑定到这个新的对象

  • 执行构造函数中的代码

  • 返回这个新的对象

省略构造函数

如果不需要任何特殊的初始化内容,可以省略构造函数,默认的构造函数被自动生成

js
class Animal {
  sleep() {
    console.log("zzzzzzz");
  }
}

const spot = new Animal();

spot.sleep(); // 'zzzzzzz'

继承

js
class Professor extends Person {
  teaches;

  constructor(name, teaches) {
    super(name);
    this.teaches = teaches;
  }

  introduceSelf() {
    console.log(
      `My name is ${this.name}, and I will be your ${this.teaches} professor.`,
    );
  }

  grade(paper) {
    const grade = Math.floor(Math.random() * (5 - 1) + 1);
    console.log(grade);
  }
}

const walsh = new Professor("Walsh", "Psychology");
walsh.introduceSelf(); // 'My name is Walsh, and I will be your Psychology professor'

walsh.grade("my paper"); // some random grade

使用extends关键字来声明这个类继承自另一个类。

构造函数中要做的第一件事是使用super()调用父类的构造函数

封装

js
class Student extends Person {
  #year;

  constructor(name, year) {
    super(name);
    this.#year = year;
  }

  introduceSelf() {
    console.log(`Hi! I'm ${this.name}, and I'm in year ${this.#year}.`);
  }

  canStudyArchery() {
    return this.#year > 1;
  }
}

#year是一个私有数据属性,类外部尝试访问#year,浏览器会抛出错误

私有方法

与私有数据属性一样,可以声明私有方法。

名称是以#开头,只能在类自己的方法中调用

js
class Example {
  somePublicMethod() {
    this.#somePrivateMethod();
  }

  #somePrivateMethod() {
    console.log("You called me?");
  }
}

const myExample = new Example();

myExample.somePublicMethod(); // 'You called me?'

myExample.#somePrivateMethod(); // SyntaxError

原型和原型链

js作者把构造函数作为原型代替class,同时规定必须满足以下条件:

  • 首字母大写

  • 内部使用this

  • 搭配new生成实例

js
function Person(name){
  this.name=name;
  this.sleep=function(){...}
}

如何区分一个function定义是构造函数还是普通函数?

  • js引入new关键字来解决

  • 因为构造函数只是定义原型(不占据内存),最终还是需要产生实例(占据内存)来处理流程,所以使用new关键字来产生实例

  • 同时规定new后面跟的是构造函数,而不是普通函数。

  • 这样就区分出定义的function是构造函数还是普通函数

js
var tom=new Person('tom');

new执行过程

1、新的对象实例被创建,占据内存

2、this指向实例,任何this上的引用最终都指向实例

3、添加__proto__属性到实例上,并把实例.__proto__指向Person.prototype

4、执行构造函数,最终返回实例对象

模拟new

js
function objectFactory(){
  var obj=new Object();
  Constructor=[].shift.call(arguments);//取出第一个参数,即构造函数
  obj.__proto__=Constructor.prototype;
  var ret=Constructor.apply(obj,arguments);
  return typeof ret==='object' ? ret : obj;
}

var tom=objectFactory(Person,'tom');
tom.name		// tom
tom.sleep  // Function

prototype

new和构造函数 模拟了 class类的概念,

使得产生的多个实例对象有共同的原型,同类型对象内在有了一些联系

但还有个问题:

每个实例对象本质上还是拷贝了构造函数对象里的属性和方法

这样每个实例的方法 创建了两个内存空间进行存储, 这样导致数据无法共享且浪费内存

解决方案:prototype属性

为构造函数设置一个prototype属性来保存这些公用方法或属性

prototype属性是一个对象,可以扩展和覆写该对象

这样通过new 构造函数()生成实例时,这些实例的公用方法不会在内存创建多份,而是通过指针都指向构造函数的prototype属性

js
# 扩展和覆写
Person.prototype.sleep=function(){...}

所以实例对象共享同一个prototype对象(构造函数的prototype属性),外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像“继承”了prototype对象一样

所以js面向对象编程基于原型

js规定,每一个构造函数都有一个prototype属性,指向另一个对象,这个对象的所有属性和方法都会被构造函数的实例继承

原型链

如何把各个实例跟构造函数的prototype对象(原型对象)联系起来的?

使用new关键字

new关键字流程中有个步骤是,添加__proto__属性到实例上,并把实例的__proto__指向构造函数的prototype

所以实例对象与原型对象间关联起来是通过__proto__属性

__proto__属性

当访问实例对象的属性或方法时,如果没有在实例上找到,js会顺着__proto__去查找原型,如果找到就返回。

由于原型对象也是一个对象,也可以有自己的原型对象,这样层层上溯,就形成了类似链表的结构,这就是原型链

JavaScript中所有的对象都有一个内置属性,称为它的prototype(原型)

它本身是一个对象,所以原型对象也有它自己的原型,逐渐构成了原型链。

原型链终止于拥有null作为其原型的对象上。

当从object中读取一个缺失的属性时,JavaScript会自动从原型中获取该属性,这被称为“原型继承”