深入JS面向对象

面向对象初识

创建对象的方式

// 方式一:通过new Object()创建
var obj = new Object()
obj.name = "hill"
obj.age = 18
obj.running = function() {
console.log(this.name + " is running");
}

// 方式二:字面量形式
var info = {
name: 'hill',
age: 18,
eating: function() {
console.log(this.name + 'is' + this.age);
}
}

对属性的操作

var obj = {
name: "yuzi",
age: 18
}

// 1.获取属性
console.log(obj.name); // yuzi

// 2.给属性赋值
obj.name = "jackson"
console.log(obj.name); // jackson

// 3.删除属性
// delete obj.name
// console.log(obj); // {age: 18}

// 4.遍历属性
for (var key in obj) {
console.log(key); // name age
}

这种直接定义在对象内部或者直接添加到对象内部的属性,我们不能对其做出限制:比如这个属性是否可以通过delete删除,是否可以在for-in遍历的时候被遍历出来

Object.defineProperty()

  • 如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符

    • 通过属性描述符可以精准的添加或修改对象的属性

    • 属性描述符需要使用 Object.defineProperty 来对属性进行添加或修改

  • Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

  • 接收三个参数

    • obj:要定义属性的对象
    • prop: 要定义或修改的属性的名称或Symbol
    • descriptor:要定义或修改的属性描述符
  • 第三个参数的属性描述符分为两类:

    • 数据属性描述符
      configurable,enumerable,writable,value
    // 直接在一个对象上定义某个属性时,描述符的默认值为
    // value: 赋值的value
    // configurable: true
    // enumerable: true
    // writable: true
    var obj = {
    name: 'jackson',
    age: 18
    }

    Object.defineProperty(obj, "height", {
    value: 1.88, // 属性值(默认为 undefined)
    enumerable: true, // 对应属性是否可以枚举(默认为false)
    writable: true, // 是否可以修改属性值(默认为false)
    configurable: true, // 属性能否被删除(默认为false),属性的描述符能否被改变
    })

    // 枚举
    for(var key in obj) {
    console.log(key); // name age height 可枚举
    }
    console.log(Object.keys(obj)); // [ 'name', 'age', 'height' ] 可枚举

    // 修改属性值
    obj.height = 2
    console.log(obj.height); // 2 可修改

    // 删除属性
    delete obj.height
    console.log(obj.height); // undefined 删除成功
    • 存取属性描述符
      enumerable,configurable,get,set

      不用value,writable,而是用 get,set

    var obj = {
    name: 'jackson',
    age: 18,
    _address: "广东"
    }
    // 私有属性,js里面是没有严格意义的私有属性的,所以我们实际上也能直接访问,但是在社区人们约定 下划线_开头的属性定义为私有属性
    // 1.隐藏某一个私有属性(address)不希望直接被外界使用和赋值
    // 2.如果我们希望截获某个属性被访问和设置值的过程时,也会使用存储属性描述符
    Object.defineProperty(obj, "address", {
    enumerable: true, // 可枚举
    configurable: true, // 可删除,可修改描述符
    get: function() {
    console.log("获取了一次address的值")
    return this._address
    },
    set: function(value) {
    console.log("设置了address的值")
    this._address = value
    }
    })

    console.log(obj.address); // 广东

可枚举属性的补充

var obj = {
name: "why",
age: 18
}

Object.defineProperty(obj, "address", {
value: "北京市"
})

console.log(obj)
// address属性默认是不可枚举的,但是我们在浏览器上面是可以看到的(稍微浅色一点),这是浏览器为了方便我们调试做的处理
image.png

Object.defineProperties()

Object.defineProperties() 方法可以直接在一个对象上定义多个新的属性

var obj = {
_age: 18
}

Object.defineProperties(obj, {
name: {
writable: true,
value: 'jackson'
},
age: {
get: function() {
return this._age
}
}
})

对象方法补充(了解)

  • 获取对象的属性描述符
    • getOwnPropertyDescriptor
    • getOwnPropertyDescriptors
  • 禁止对象扩展新属性
    • preventExtensions:给一个对象添加新的属性会失败(在严格模式下会报错)
  • 密封对象,不允许配置和删除属性:seal
    • 实际是调用preventExtensions
    • 并且将现有属性的configurable:false
  • 冻结对象,不允许修改现有属性:freeze
    • 实际上是调用seal
    • 并且将现有属性的writable: false

创建多个对象的方式

通过想要创建多个对象的目的,来引出后面的构造函数

前面我们通过 new Object,字面量的方式创建对象,但是这两种方式有一个很大的弊端:

创建同样的对象时,需要编写重复代码

比如说字面量:

var p1 = {name: , age: }
var p2 = {name: , age: }
var p3 = {name: , age: } // 它们有同样的属性或者方法

创建对象的方式 - 工厂模式

  • 工厂模式其实是一种常见的设计模式
  • 通常我们会有一个工厂方法,通过该工厂方法我们可以产生想要的对象
  • 工厂方法创建对象有一个比较大的问题:我们在打印对象时,对象的类型都是Object类型
  • 但是从某些角度来说,这些对象应该有一个他们共同的类型
function createPerson(name, age, height) {
var p = {} // 定义一个对象
p.name = name
p.age = age
p.height = height

p.eating = function() {
console.log(this.name + "is eating");
}

return p // 把对象返回
}

var p1 = createPerson("张三", 18, 1.88)
var p2 = createPerson("李四", 20, 1.98)
var p3 = createPerson("王五", 30, 1.78)

// 工厂模式的缺点:获取不到对象最真实的类型
console.log(p1, p2, p3); // 我只知道你是一个对象,但我不知道你是person类型

认识构造函数

什么是构造函数?

  • 构造函数也称之为构造器(constructor),通常我们在创建对象时会调用的函数
  • JavaScript 中,如果一个普通的函数被使用 new 操作符来调用了,那么这个函数就称之为是一个构造函数

所以说构造函数也是一个普通函数,只不过用 new 去调用,就称为构造函数

new也是可以调用函数的喔

new操作符

如果一个函数被使用new操作符调用了,那么它会执行如下操作:

  1. 在内存中创建一个新的对象(空对象)
  2. 这个对象内部的 **[[prototype]]**属性会被赋值为该构造函数的 prototype属性
  3. 构造函数的 this ,会指向创建出来的新对象
  4. 执行函数的内部代码
  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象

构造函数创建对象

// 规范:构造函数的首字母一般是大写
function Person(name, age, height, address) {
this.name = name
this.age = age
this.height = height
this.address = address

this.eating = function() {
console.log(this.name + "is eating");
}
this.running = function() {
console.log(this.name + 'is running');
}
}

var p1 = new Person("张三", 18, 1.88, '广州')
var p2 = new Person("李四", 20, 1.78, '北京')

console.log(p1); // Person {...} 是可以看见类型的
console.log(p2);
  • 这个构造函数可以确保我们的对象是有Person的类型的
  • 但是也有缺点:我们需要给每个对象的函数去创建一个函数对象实例(开辟新的内存空间)

对象的原型 [[prototype]]

  • JavaScript 中,每个对象都有一个特殊的内置属性 [[prototype]],这个属性称之为对象的原型(隐式原型)(只要是对象,就会有这个内置属性)

  • [[prototype]] 指向一个对象(也就是说它也是一个对象)

  • 那么这个对象有什么用呢?

    • 当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作
    • 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它
    • 如果对象中没有该属性, 那么会沿着它的原型去查找 [[prototype]]
  • 如果通过字面量直接创建一个对象,那么这个对象也有[[prototype]]这个属性

    如何查看这个属性呢

    var obj = { name: "jackson"}
    var info = {}
    // 创建出来的对象上都有 [[prototype]]属性

    // 如何查看这个属性?
    // 早期的ECMA是没有规范如何去查看 [[prototype]]

    // 给对象中提供了一个属性 __proto__, 可以让我们查看一下这个原型对象(浏览器提供)
    console.log(obj.__proto__);// [Object: null prototype] {}

    // 相当于(伪代码)
    var obj = {name: 'jackson', __proto__: {}}

    // ES5之后提供的Object.getPrototypeOf()查看
    console.log(Object.getPrototypeOf(obj)); // {}

    // 例如找 age 这个属性,该对象本身没有,沿着原型查找
    obj.__proto__.age = 18
    console.log(obj.age); // 18

函数的原型 prototype

  • 所有的函数都有一个 prototype 属性(显式原型)
function Foo() {
}

// 函数也是一个对象,所以它也是有[[prototype]]隐式属性
// 另外,函数还会多出来一个显式原型属性:prototype
console.log(Foo.prototype);
  • 再看回new操作符其中的一个步骤:

    这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性

  • 也就意味着我们通过 Foo 构造函数创建出的所有对象的 [[prototype]] 都指向 Foo.prototype

var f1 = new Foo()
var f2 = new Foo()

console.log(f1.__proto__ === Foo.prototype); // true
console.log(f2.__proto__ === Foo.prototype); // true

创建对象的内存表现

image.png

可以看到:

  • 构造函数的 prototype 属性指向该函数的原型对象
  • 原型对象身上有一个 constructor 属性指回构造函数本身
  • new出来的实例对象 p1,p2 对象身上有__proto__属性也指向构造函数的原型对象

函数原型上的属性constructor

  • 默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象
function Foo() {}

// 1. constructor属性
// Foo.prototype这个对象中有一个constructor属性
console.log(Foo.prototype); // {} 为什么没看到constructor?
// 那换一种方式看
console.log(Object.getOwnPropertyDescriptors(Foo.prototype));
//打印
// {
// constructor: {
// value: [Function: Foo],
// writable: true,
// enumerable: false,
// configurable: true
// }
// }
// 可以看到enumerable为false,所以第一种方式不能看到constructor

// 既然这样,我们能不能重写这个对象的属性constructor呢?肯定可以呀
Object.defineProperty(Foo.prototype, "constructor", {
enumerable: true, //可枚举
configurable: true, // 可删除,描述符可修改
writable: true, // 可写
value: 'hahahha'
})
// 这个时候我们再来直接打印,就可以看到constructor了(被我们改成可枚举了)
console.log(Foo.prototype);// { constructor: 'hahahha' }
  • 我们也可以在prototype上添加自己的属性
Foo.prototype.name = "jackson"
Foo.prototype.age = 18
Foo.prototype.eating = function() {}

// 我们再new对象的时候,这些属性都可以通过实例的原型__proto__找到
var f1 = new Foo()
console.log(f1.name); // jackson
console.log(f1.__proto__);
// 打印
// {
// constructor: 'hahahha',
// name: 'jackson',
// age: 18,
// eating: [Function (anonymous)]
// }
image.png
  • 直接修改整个prototype对象(赋值,新开内存空间)
Foo.prototype = {
constructor: Foo, // 让它指回本身(但这样它默认是可枚举的)
name: "jackson",
age: 20
}

var f1 = new Foo()
console.log(f1.name); // "jackson"

// 为了可以自定属性描述符,真实开发中我们可以通过Object.defineProperty添加constructor
Object.defineProperty(Foo.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: Foo
})
image.png

原来的foo函数的原型对象因为没有对它的引用,会被回收的

创建对象的方式 — 构造函数和原型组合

function Person(name, age, address) {
this.name = name
this.age = age
this.address = address
// this.fn = function()
// 为了不给每个实例都新开内存空间保存相同的方法,我们不把共同的方法定义在这
}

// 我们可以把一些公共的方法直接通过原型对象添加
Person.prototype.eating = function() {
console.log(this.name + 'is eating');
// 这里的this怎么找到实例对象的?
// 调用函数的时候隐式绑定啊
}

Person.prototype.running = function() {
console.log(this.name + 'is running');
}

var p1 = new Person("jackson", 18, "guangdong")
var p2 = new Person("yuzi", 18, "beijing")
p1.eating()
p2.eating()

JavaScript中的类和对象

function Person() {}

在 JS 中,Person应该被称之为是一个构造函数

但是从很多面向对象语言的开发者习惯称之为类,因为类可以帮我们创建出来实例对象,也是可以的

面向对象的特性 - 继承

面向对象有三大特性:封装、继承、多态

  • 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程
  • 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中)
  • 多态:不同的对象在执行时表现出不同的形态

JavaScript原型链

原型链的理解

var obj = {
name: "jackson",
age: 18
}

// 当我们"."的时候就是[[get]]操作
// 1.在当前的对象中查找属性
// 2.如果没有找到,这个时候会去原型链(__proto__)对象上查找
obj.__proto__ = {
// address: "广州市"
}

// 原型链
obj.__proto__.__proto__= {
// address: "广州市"
}

obj.__proto__.__proto__.__proto__= {
address: "广州市"
}

console.log(obj.address); // "广州市"
// 只要在原型链上都可以找到的
image.png

这样一直向上找的话,那么顶层原型究竟是什么呢?

var obj = {name: "Jackson"}

// 找到那一层对象之后会停止继续查找呢?
console.log(obj.__proto__);
// 可以看到字面量obj的原型是 [Object: null prototype] {}

// 我们继续往上看看
console.log(obj.__proto__.__proto__); // null
// 所以我们可以说 [Object: null prototype] {} 就是顶层的原型

顶层对象有什么特别吗?该对象上有很多默认的属性和方法

顶层原型又来自哪里呢?下面我们创建Object对象看看

// 创建一个对象(这种方式相当于下面一种方式的语法糖,本质都是创建一个对象)
var obj1 = {}
var obj2 = new Object() // 创建了一个对象
// 创建对象的话,其中有一步是:将Object函数的显式原型prototype赋值给实例的隐式原型
// 相当于这里
// Object.prototype = obj2.__proto__

// 因此(我们知道这里obj1/2.__proto__已经是到顶层了)
console.log(Object.prototype === obj1.__proto__); // true
console.log(Object.prototype === obj2.__proto__); // true

// Object.prototype也是一个对象,那么它应该也有 __proto__
console.log(Object.prototype.__proto__); // null
// 因此Object.prototype指向的已经是顶层原型了

console.log(Object.getOwnPropertyDescriptors(Object.prototype))
// 有constructor,toString等属性和方法

因此,原型链最顶层的原型对象就是Object的原型对象

image.png

也就是说,Object是所有类的父类

function Person(name) {
this.name = name
}

// 看看构造函数Person的原型
console.log(Person.prototype);// {} 看不到!应该是不可枚举吧
console.log(Object.getOwnPropertyDescriptors(Person.prototype));
//输出 {constructor: {...}}

console.log(Person.prototype.__proto__);//[Object: null prototype] {}
image.png

通过原型链实现继承

为什么需要有继承?

如果没有继承,我们想创建多个类,类里面的属性和方法很多是一样的,那么我们就会写很多重复的代码,所以主要是为了代码的复用

// Student
function Student(name, age, sno) {
this.name = name
this.age = age
this.sno = sno
}
Student.prototype.running = function() {}
// Teacher
function Teacher(name, age, title) {
this.name = name
this.age = age
this.title = title
}
Teacher.prototype.running = function() {}

原型链的继承方案

自己画画图更好理解

// 父类:公共属性和方法
function Person() {
this.name = "Jackson",
this.friends = []
}
Person.prototype.eating = function() {
console.log(this.name + "is eating");
}

// 子类:特有属性和方法
function Student() {
this.sno = 111
}

// new一个person
var p = new Person()
Student.prototype = p

var stu = new Student()
// console.log(stu.sno); // 111
// console.log(stu.name);// Jackson
// stu.eating()

// 原型链实现继承的弊端:
// 1.打印stu对象,继承的属性是不能直观看到的
// console.log(stu); // Person { sno: 111 }
// 前面显示的 Person 类型,实际上是实例的name属性,显然这里应该是Student

// 2.创建出来两个stu对象
var stu1 = new Student()
var stu2 = new Student()

// 获取引用,修改引用中的值,会相互影响
// 因为这个fre1被加到p对象,而stu1,stu2的__proto__都指向p
stu1.friends.push("fre1")
console.log(stu1.friends); // [ 'fre1' ]
console.log(stu2.friends); // [ 'fre1' ]

// 3. 在前面实现类的过程中都没有传递参数
var stu3 = new Student("jeccy", 112)

// 直接在function Student(name,age){this.name = name this.age = age}?
// 肯定不行啊,我们是想要把name的处理放在Person的

借用构造函数实现继承

使用call调用构造函数,

// 父类:公共属性和方法
function Person(name, age, friends) {
this.name = name,
this.age = age
this.friends = friends
}
Person.prototype.eating = function() {
console.log(this.name + "is eating");
}

// 子类:特有属性和方法
function Student(name, age, friends, sno) {
// 我们在这里调用Person,并把需要Person处理的参数传过去
// this 就是Student的实例
Person.call(this, name, age, friends) // 这里可以获得父类的属性
this.sno = sno
}

// new一个person
var p = new Person() // 依然需要这里来获得方法
Student.prototype = p

// 解决弊端3
var stu1 = new Student("aaa", 18, ['fred1'], 111)
console.log(stu1);
// Person { name: 'aaa', age: 18, friends: [ 'fred1' ], sno: 111 } 解决弊端1
var stu2 = new Student("vvv", 20, ['fred2'], 112)
stu1.friends.push("hahaha")
console.log(stu1.friends); // [ 'fred2' ]
console.log(stu2.friends); // [ 'fred1', 'hahaha' ]
// 解决弊端2

但是这种方法依然存在弊端:

  1. Person 至少被调用两次(一开始new Person一次,后面Person.call又会调用Person)
  2. stu的原型对象上会多出一些属性, 但是这些属性是没有存在的必要(new Person的时候的)
image.png

注意:

那么我们换一种获得父类方法的方法,直接将父类原型赋值给子类

肯定是不行的,因为以后给某个子类添加方法的时候,会使所有的子类都有该方法,显然是不行的

因为所有子类的prototype都指向同一个父类的原型

原型式继承函数 - 对象

我们先实现对象的继承,后面再扩展到类

var obj = {
name: "Jackson",
age: 18
}

// 原型式继承函数
// 这个函数要做到的是,你给我传入的对象,作为新对象的原型
function createObject(o) {
var newObj = {}
// 这个方法是把o设置为newObj的原型
Object.setPrototypeOf(newObj, o)
return newObj
}

// Douglas 的实现(当时还没有setPrototypeOf这个方法)
function createObject2(o) {
function Fn() {}
Fn.prototype = o
var newObj = new Fn()
// 因此 newObj.__proto__ = Fn.prototype = o
return newObj
}

// 现在,我们想要新建的对象info的原型指向obj(后面扩展到类)
// var info = createObject(obj)

// 但是 新的ECMA 给我们提供了新的方法:Object.create(obj)
// Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
// 实际上这个方法跟我们上面的两种实现方法实现的功能是一样的
var info = Object.create(obj)

console.log(info);
// 已经实现了我们的目的
console.log(info.__proto__); // { name: 'Jackson', age: 18 }

Object.create() :https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create

寄生式继承函数(了解)

  • 寄生式继承的思路是结合原型类继承和工厂模式的一种方式

  • 即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回

var personObj = {
running: function() {
console.log('running');
}
}

// 目的:继承 personObj里面的方法
function createStudent(name) {
var stu = Object.create(personObj)
stu.name = name
stu.studying = function() {
console.log("studying");
}
return stu
}

var stuObj = createStudent("why")

寄生组合式继承(最终方案)

利用寄生式继承将组合式继承的两个问题解决

  • 首先我们需要明确,当我们在子类的构造函数中调用父类.call(this,参数)的时候,就会将父类的属性和方法复制一份到子类中,所以父类本身里面的内容我们是不需要的
  • 然后,我们还需要获取到一份父类的原型对象中的属性和方法
// 如果不想使用Object.create这个方法的话,我们可以定义前面说过的方法
function createObject(o) {
function Fn() {}
Fn.prototype = o
return new Fn()
}

function inheritPrototype(SubType, SuperType) {
// SubType.prototype = Object.create(SuperType.prototype)
SubType.prototype = createObject(SuperType.prototype)
// 当然子类的prototype还需要有constructor指向子构造函数本身
Object.defineProperty(SubType.prototype, "constructor", {
enumerable: false,
configurable: true,
writable: true,
value: SubType
})
}

// 父类
function Person(name, age, friends) {
this.name = name
this.age = age
this.friends = friends
}

Person.prototype.running = function() {
console.log("running~")
}

// 子类
function Student(name, age, friends, sno) {
Person.call(this, name, age, friends) // 获取一份Person中的属性和方法
this.sno = sno
}

// Student类还需要获取一份父类的prototype的属性和方法
inheritPrototype(Student, Person)

var stu = new Student("why", 18, ["kobe"], 111)
console.log(stu);
// 打印 Student { name: 'why',age: 18,friends: [ 'kobe' ],sno: 111,}

JS原型的补充

hasOwnProperty

  • 判断对象是否有某个属于自己的属性,不包括在原型上的

in/ for in 操作符

  • 判断某个属性是否在某个对象或者对象的原型上
var obj = {
name: 'jackson',
age: 18
}

// 第二个参数是info添加属于自己的属性
var info = Object.create(obj, {
address: {
value: "北京",
enumerable: true
}
})
// hasOwnProperty
console.log(info.hasOwnProperty("address")); // true
console.log(info.hasOwnProperty("name")); // false

// in 操作符: 不管在当前对象还是原型中返回的都是true
console.log("address" in info); // true
console.log("name" in info); // true

// for in (包括原型上的属性都可以遍历到)
for(var key in info) {
console.log(key); // address name age
}

instanceof

用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

function Person() {}
function Student() {}
// 使用前面的寄生组合式继承
inheritPrototype(Student, Person)
console.log(Person.prototype.__proto__); // [Object: null prototype] {}

var stu = new Student()
// stu是否出现在构造函数 Student 的原型链上
console.log(stu instanceof Student); // true
console.log(stu instanceof Person); // true
console.log(stu instanceof Object); // true

isPrototypeOf(了解)

用于检测某个对象,是否出现在某个实例对象的原型链上

var info = Object.create(obj)
// obj是否出现在info的原型链上
console.log(obj.isPrototypeOf(info))

对象-函数-原型之间的关系

var obj = {
name: "jackson"
}
// 只要是对象,里面就会有一个__proto__对象(隐式原型对象)
console.log(obj.__proto__);// [Object: null prototype] {}

function Foo() {}
// 所有的 function xx() {}
// 我们都可以认为 xx 是 new Function 创建出来的
// 相当于 var xxx = new Function

// 只要是函数,那么它就会有一个显式原型对象:Foo.prototype
// 那 Foo.prototype 这个对象又来自哪里呢?
// 创建函数的时候,JS内部就会创建一个对象,并添加到函数的prototype属性中:Foo.prototype = {constructor: Foo}

// Foo也是一个对象,只要是对象,就会有隐式原型对象 Foo.__proto__
// Foo.__proto__来自哪里?
// var Foo = new Function()
console.log(Foo.__proto__ === Function.prototype); // true
// 而 Function.prototype对象是我们创建Function函数的时候创建出来的
// Function.prototype = {constructor: Function}

// 创建Function函数
// function Function() {}
// Function.prototype
// Function 又是一个对象
// Function.__proto__
// 唯一一个比较特殊的东西
console.log(Function.__proto__ === Function.prototype); // true

// 另外还有 function Object() {}
// Object作为函数, 就会有Object.prototype
// Object作为对象, 就会有Object.__proto__
// Object函数是Function创建出来的,所以
// Objcet.__proto__ === Function.prototype

// 最后就是每个函数的原型prototype都会有一个constructor指回函数本身

最后 上图!

image.png image.png