大白话系列之JavaScript原型及面向对象

对象

前言

一开始就和Java一样整个new

1
2
3
4
5
6
7
8
// 1.创建一个空的对象
var obj1 = new Object()
obj1.name = "xiaoyu"
obj1.age = 18
obj1.height = 1.88
obj1.eating = function() {
  console.log(this.name + "在吃东西")
}

后来很多开发者为了方便期间,都是直接通过字面量的形式来创建对象:

1
2
3
4
5
6
7
8
var obj2 = {
  name: "kobe",
  age: 40,
  height: 1.98,
  running: function() {
    console.log(this.name + "在跑步")
  }
}

对象属性描述

如果使用字面量形式进行声明对象属性,我们就失去了对对象属性的精准控制。

如:

  • 这个属性我能不能delete
  • 这个属性能不能在for in的时候被遍历

为了实现如此控制我们可以使用Object.defineProperty来对属性进行添加或者修改

而数据属性描述又分为两类

  • 数据属性描述

    • [[Configurable]]:表示属性是否可以通过delete删除属性,是否可以修改它的特性。

      • 当我们在一个对象上定义了某个属性时,这个属性的Configurable为true
      • 当我们通过属性描述符(Object.defineProperty)定义一个属性时,这个值为false
    • [[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性;

      • 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true;
      • 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false;
    • [[Writable]]:表示是否可以修改属性的值;

      • 当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true;
      • 当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false;
    • [[value]]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改;

      • 默认情况下这个值是undefined;
  • 存取属性描述

    • [[Configurable]] [[Enumerable]] 这两个似乎可以相互转换,功能是一样的。
    • [[get]]:获取属性时会执行的函数。默认为undefined
    • [[set]]:设置属性时会执行的函数。默认为undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var obj = {
  name"xiaoyu",
  age18,
  height1.88
}

// 默认是可以配置
// delete obj.name
// console.log(obj)

for (var key in obj) {
  console.log(key)
}

console.log(Object.keys(obj))

obj.name = "kobe"
console.log(obj)

如果自己定义属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// "use strict"

var obj = {
  name: "xiaoyu",
  age: 18,
  height: 1.88
}

// 自己定义的属性
Object.defineProperty(obj, "address", {
  // configurable: false,
  // enumerable: false,
  // writable: false,
  value: "北京市"
})

// 1.测试enumerable为false
// 这种方式访问时看不到属性
console.log(obj)
console.log(Object.keys(obj))
for (var key in obj) {
  console.log(key)
}
// 这种方式是可以访问的
console.log("address" in obj)
console.log(obj.hasOwnProperty('address'))
console.log(obj.address)

// 2.测试writable, 修改address的值
obj.address = "广州市"
// 北京市, 并且在严格模式下会报错
console.log(obj.address)

// 3.测试configurable
// 不可以删除
delete obj.address
// 不可以重新修改
Object.defineProperty(obj, 'address', {
  configurable: true
})

console.log(obj.address)

…… 省略一把

构造函数创建对象

什么是构造函数?

  • 就是在我们创建对象时会调用的函数。
  • 在其他面向的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法;
  • 但是JavaScript中的构造函数有点不太一样

构造函数在js中就是一个普通函数,和别的函数没有区别

被new关键词调用的函数,就是构造函数

什么是new?

如果一个函数被new操作符调用了就会有如下操作:

  1. 内存中创建一个新的对象(空对象)
  2. 这个对象内部的Property属性会被赋值为该函数的Property属性。
  3. 构造函数内部的this会指向创建出来的新对象。
  4. 执行函数的内部代码也就是函数体代码
  5. 如果构造函数没有返回非空对象,那么返回新创造的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person() {
}

var p1 = new Person()
var p2 = new Person()



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 + "在吃东西~")
  }
  this.running = function() {
    console.log(this.name + "在跑步~")
  }
}

这个构造函数可以确保我们的对象是有Person的类型的(实际是constructor的属性,这个我们后续再探讨);

认识原型

JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象。

  • 当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作;
  • 这个操作会先检查是否有对应的属性,有就用
  • 如果对象中没有改属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;
  • 这个 [[prototype]] 我们通常会将其称之为隐式原型;

那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?

答案是有的

  • 方式一:通过对象的 __proto__ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);

  • 方式二:通过 Object.getPrototypeOf 方法可以获取到;

1
2
3
4
5
6
7
var obj = {}

// 方式一: __proto__(有浏览器兼容问题)
console.log(obj.__proto__)

// 方式二: Object.getPrototypeOf
console.log(Object.getPrototypeOf(obj))

那么我们就可以进行如下的测试了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个obj对象
var obj = {}

// 直接给对象添加address属性
// obj.address = "北京市"

// 直接给隐式原型上添加address属性
// 给__proto__上添加address属性
obj.__proto__.address = "广州市"

// 通过Object.setPrototypeOf来设置隐式原型
Object.setPrototypeOf(obj, { address: "上海市", name: "setPrototypeOf" })

console.log(obj.address)

结论,我们是可以直接这样子去添加的。

函数的原型

我们前面说了,通过构造函数创建对象时,对象的Property会是构造函数的Property

函数的prototype

这里我们又要引入一个新的概念:所有的函数都有一个prototype的属性:

1
2
3
4
5
6
function foo() {

}

// 所有的函数都有一个属性, 名字是 prototype 
console.log(foo.prototype)

不是因为函数是一个对象所以有prototype属性。

而是因为是函数才有prototype属性。

对象没有prototype属性,有的是 __proto__这个属性

我们前面讲过new关键字的步骤如下:

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

我们将重心放到步骤一和二中:

  • 在内存中创建一个对象;
  • 将对象的[[prototype]]属性赋值为该构造函数的prototype属性;

那就意味着:

1
2
3
4
5
6
7
8
9
10
function Person(){

}

var a = new Person()

// 有一步操作是

a.__proto = Person.prototype

那么也就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype:

1
2
3
4
5
6
7
8
9
10
function Person() {

}

var p1 = new Person()
var p2 = new Person()
var p3 = new Person()

console.log(p1.__proto__ === p2.__proto__)
console.log(p1.__proto__ === Person.prototype)

那么其实就是..Person函数有一个prototype属性指向 Person的原型。

然后对象创建出来以后..对象的 __proto__和指向和Person函数一样。

image-20240906162418938

那么如果在Person的prototype属性中创建属性,那么p的对象能不能访问到?

当然可以

1
2
3
4
5
6
7
8
9
10
11
12
function Person() {

}

Person.prototype.name = "xiaoyu"
Person.prototype.age = 18

var p1 = new Person()
var p2 = new Person()

console.log(p1.name, p1.age)
console.log(p2.name, p2.age)

值得一提的是,如果你中途来一个p1.name = xxx 那么此时会在你p1的对象里面创建一个name,并不会改变Person中的值

constructor属性

事实上原型对象(就是指函数的prototype属性指向的原型对象)上面是有一个属性:constructor。

默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象(这里指的是函数的prototype指向的对象中的constructor的值又是该函数本身,也就是Person函数的prototype对象中的constructor的值指向的是Person函数【函数tm的在js的内存中是对象形式存在所以妈的这句话就很拗口】);

1
2
3
console.log(Person.prototype.constructor// [Function: Person]
console.log(p1.__proto__.constructor// [Function: Person]
console.log(p1.__proto__.constructor.name// Person

重写原型

如果我们需要在原型上添加很多很多属性,通常就重写原型了。

1
2
3
4
5
6
7
8
9
10
11
function Person() {

}

Person.prototype = {
  name"xiaoyu",
  age18,
  eatingfunction() {
    console.log(this.name + "在吃东西~")
  }
}

我们说过..每创建一个函数就会生成一个函数对应的原型对象。而原型对象的constructor也会自动获取到函数对象(这里的函数对象其实也就是Person Fuction哈)。

但是我们如果重写了函数的prototype,那么这个prototype就会默认指向Object构造函数了(Object Function)。

1
2
3
4
console.log(Person.prototype.constructor// [Function: Object]
// 为什么是Object呢? 因为对象的字面量是由Object函数产生的
var obj = {}
console.log(obj.constructor// // [Function: Object]

如果要原型constructor指向回来,可以如下

1
2
3
4
5
6
7
8
Person.prototype = {
  constructor: Person,
  name: "xiaoyu",
  age: 18,
  eating: function() {
    console.log(this.name + "在吃东西~")
  }
}

除了constructor属性一些特性被默认开启了如

[[Enumerable]]为true了。

不然就只能用Object.defineProperty啦

1
2
3
4
5
6
7
8
9
10
11
12
Person.prototype = {
  name"xiaoyu",
  age18,
  eatingfunction() {
    console.log(this.name + "在吃东西~")
  }
}

Object.defineProperty(Person.prototype"constructor", {
  enumerablefalse,
  valuePerson
})

原型链

在真正实现继承之前,我们先来理解一个非常重要的概念:原型链。

我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取:

1
2
3
4
5
6
7
8
9
10
var obj = {
  name"xiaoyu",
  age18
}

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

console.log(obj.address)

但是如果obj的原型上也没有对应的address属性呢?必然还是获取不到的。

那么如果我们配置的原型对象上,继续配置原型呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var obj = {
  name"xiaoyu",
  age18
}

obj.__proto__ = {
}

obj.__proto__.__proto__ = {

}

obj.__proto__.__proto__.__proto__ = {
  address"北京市"
}

console.log(obj.address)

image-20240906164844964

Object的原型

按照上面的案例,第三个proto的proto就是Object对象了

1
console.log(obj.__proto__.__proto__.__proto__.__proto__) // [Object: null prototype] {}

看一下默认的对象原型都是啥

1
2
3
4
5
6
7
8
9
var obj = { name"xiaoyu" }
console.log(obj.__proto__// [Object: null prototype] {}

var obj1 = new Object()
console.log(obj1.__proto__// [Object: null prototype] {}
console.log(Object.prototype// [Object: null prototype] {}

console.log(obj.__proto__ === Object.prototype// true
console.log(obj1.__proto__ === Object.prototype// true

我们可以知道,从Object直接创建出来的对象的原型都是 [Object: null prototype] {}

那么我们可能会问题: [Object: null prototype] {} 原型有什么特殊吗?

  • 特殊一:该对象不再继续有原型属性了,也就是已经是顶层原型了;
  • 特殊二:该对象上有很多默认的属性和方法;

image-20240906165201726

继承

ES6下Class语法糖

拯救以上你一下子学不会,写啊写不明白的原型继承,虽然底层依旧是我上面讲的。

那么,如何使用class来定义一个类呢?

1
2
3
4
5
6
7
8
9
// 类的声明
class Person {

}

// 类的表达式
var Student = class {
  
}

接着我们就可以使用new操作符调用类:

1
2
3
var p1 = new Person
var p2 = new Person
console.log(p1, p2)

这个写法创造的变量,其实和我们刚刚new 函数干出来的其实是一样的

1
2
3
4
5
6
7
8
9
var p = new Person

console.log(Person// [class Person]
console.log(Person.prototype// {}
console.log(Person.prototype.constructor// [class Person]

console.log(p.__proto__ === Person.prototype// true

console.log(typeof Person// function

类的构造方法(从这开始就和你其他oop学到的差不多了)

如果我们希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢?

  • 每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor;
  • 当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor;
  • 每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常;
1
2
3
4
5
6
7
8
9
10
class Person {
  constructor(name, age, height) {
    this.name = name
    this.age = age
    this.height = height
  }
}

var p1 = new Person("xiaoyu"181.88)
console.log(p1)

通过new关键字操作的时候..其实和上文学习的内容是一样的

当我们通过new关键字操作类的时候,会调用这个constructor函数,并且执行如下操作:

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

相比较于之前直接写this复制那一套,语法糖果然名副其实

类的方法定义

在上面我们定义的属性都是直接放到了this上,也就意味着它是放到了创建出来的新对象中:

  • 在前面我们说过对于实例的方法,我们是希望放到原型上的,这样可以被多个实例来共享;
  • 这个时候我们可以直接在类中定义;(其实不要觉得怪异,这在oop思想中本就是应该的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
  constructor(name, age, height) {
    this.name = name
    this.age = age
    this.height = height
  }

  running() {
    console.log(this.name + " running~")
  }

  eating() {
    console.log(this.name + " eating~")
  }
}

var p1 = new Person("xiaoyu"181.88)
console.log(p1)

p1.running()
p1.eating()
// [ 'constructor', 'running', 'eating' ]
console.log(Object.getOwnPropertyNames(Person.prototype))

这样子就可以在各个对象中进行使用了,因为是放在原型上的。

我们也可以查看它们的属性描述符:

  • 会发现它们的enumerable都是为false的;

访问器方法

我们之前讲对象的属性描述符有时可以添加setter和getter函数的,那么类也是可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
constructor(name) {
this._name = name
}

set name(newName) {
console.log("调用了name的setter方法")
this._name = newName
}

get name() {
console.log("调用了name的getter方法")
return this._name
}


}


var p = new Person("张三")
console.log(p.name)
// 这样子写以后,我们可以在打印p.name的时候发现调用了setter方法
p.name = "李四" // 对p.name进行更改,会触发setter
console.log(p.name)

ok 就差不多就是这个写法吧。

但是和直接在对象中定义不同的是,类中的setter和getter是放在对象的

proto上面的,不是和对象中定义一样放在对象中。

image-20240908184527958

我们可以发现确实是放在原型上的哦~

ps:我们如果直接在node环境下进行打印prototype是不能显示的,console.log会有显示上限的噢

所以我们可以用getOwnPropertyDescriptors如下方法进行打印

1
console.log(Object.getOwnPropertyDescriptors(p.__proto__))

getOwnPropertyDescriptors 方法返回一个对象,其中包含指定对象的所有自身属性(即直接属性)的描述符。每个属性的描述符包括 value, writable, enumerable, 和 configurable 等属性。

静态方法

静态方法就和你在别的语言里面所了解的到的一样,比如PHP和Java都有静态方法。

也就是可以直接用类去调用方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
  constructor(age) {
    this.age = age
  }

  static create() {
    return new Person(Math.floor(Math.random() * 100))
  }
}

for (var i = 0; i < 10; i++) {
  console.log(Person.create())
}

ES6类继承

没错就是extends,ES5的继承太抽象了,要这么繁琐,所以ES6直接一手extends

1
2
3
4
5
6
7
8
class Person{

}

class Student extends Person{

}

我们知道继承可以让我们复用父类的一些代码结构,比如继承属性和方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

  running() {
    console.log(this.name + " running~")
  }

  eating() {
    console.log(this.name + " eating~")
  }
}

class Student extends Person {
  constructor(name, age, sno) {
    super(name, age)
    this.sno = sno
  }

  studying() {
    console.log(this.name + " studying~")
  }
}

var stu = new Student("xiaoyu"18111)

super关键字

如果在子类的构造函数中使用this或者返回默认对象之前,必须调用super调用父类的构造方法。

这一点PHP的面向对象里面如果要调用父类函数有一个parent::写法,或者直接$this起手,如果子类没有就会去找父类而没有super这个函数。

super的使用位置有三个:

  1. 子类的构造函数
  2. 实例方法
  3. 静态方法;
1
2
3
4
5
// 调用 父对象/父类 的构造函数
super([arguments]);

// 调用 父对象/父类 上的方法
super.functionOnParent([arguments]);

下面的代码会报错,因为我们没有调用super:

1
2
3
4
5
6
7
8
9
10
11
class Person {

}

class Student extends Person {
  constructor(sno) {
    // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
  }
}

var stu = new Student()

我们可以在子类的方法中调用父类的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Person {
constructor(name, age) {
this.name = name
this.age = age
}

running() {
console.log(this.name + " running~")
}

eating() {
console.log(this.name + " eating~")
}
static create() {
console.log("hello world")
}
}

class Student extends Person {
constructor(name, age, sno) {
super(name, age)
this.sno = sno
}

studying() {
console.log(this.name + " studying~")
}
running() {
super.running()
console.log("this is student reload running~", this.name)
}
static create() {
super.create()
console.log("aaaaaaa")
}
}

var stu = new Student("xiaoyu", 18, 111)
stu.running()
Student.create()

继承内置类

emm 这个概念怎么说呢,如果你想要对Array类做出一些改变加个方法什么的也不是不行,这样子你就可以用到内置类。

比如Array

1
2
3
4
5
6
7
8
9
10
11
12
class xArray extends Array{
lastItem(){
return this[this.length-1]
}
}

var array = new xArray(10,20,30)
console.log(array.lastItem())

array.filter(item => {
  console.log(item)
})

但是目前这种继承所返回的数据,或者数据类型既是xArray又是Array我们可以做个改动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class xArray extends Array {
lastItem() {
return this[this.length - 1]
}
static get [Symbol.species](){
return Array
}
}

var array = new xArray(10, 20, 30)
console.log(array.lastItem())

var demo = array.filter(item => {
console.log(item)
})

console.log(demo instanceof xArray)
console.log(demo instanceof Array)

ES6对象增强

ES6中对对象字面量进行了增强

属性简写

就对象属性赋值的时候,如果变量名和属性名一样就可以简略些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var name = "xiaoyu"
var age = 18

// ES5的写法
var obj1 = {
  name: name,
  age: age
}

// ES6的增强写法
var obj2 = {
  name,
  age
}

方法简写

另一个就可以方法简写,其实在不知不觉中我自己已经有混用发现了…..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ES5的方法写法
var info1 = {
  foofunction() {

  },
  barfunction() {

  }
}

// ES6的增强写法
var info2 = {
  foo() {

  },
  bar() {
    
  }
}

计算属性名

相信你一定有遇到属性名是需要动态构成的情况,比如我一个属性名得是一个变量名对吧~以前es5只能字面量赋值

1
2
3
4
5
6
7
var name = "three"
var obj = {
  one1,
  two2
}

obj[name] = 3

ES6可以

1
2
3
4
5
6
7
8
var name = "three"
var obj = {
  one1,
  two2,
  [name]: 3
}

console.log(obj)

解构Destructuring

ES6加了一个结构的方法,咋说呢,要不是去学习我以为就是天然支持的哈哈哈,毕竟是别的

语言过来的,很多东西都是相通的。

数组结构

1
2
3
4
var names = ["abc""cba""nba"]

var [name1, name2, name3] = names
console.log(name1, name2, name3)

但是结构数组必须得按照顺序来,如果你只要第二个和第三个

1
2
var [, nameb, namec] = names
console.log(nameb, namec)

隔壁GO是_来占位,js直接空着就完事了

如果我们希望解构出来一个元素,其他元素继续放到另外一个数组中:

1
2
var [namea, ...newNames] = names
console.log(namea, newNames)

剩下两个就丢进了newNames中了

如果我们解构的数据数量大于数组中原本的数据数量,那么会返回undefined:

1
2
var [namex, namey, namez, namem] = names
console.log(namem) // undefined

我们可以在解构出来的数据为undefined的时候给它一个默认值

1
2
3

var [namex, namey, namez, namem = "aaa"] = names
console.log(namem) // aaa

对象的解构

对象的解构和数组的解构是相似的,不同之处在于:

  • 数组中的元素是按照顺序排列的,并且我们只能根据顺序来确定需要获取的数据;
  • 对象中的数据由key和value组成,我们可以通过key来获取想要的value;
1
2
3
4
5
6
7
8
var obj = {
  name"xiaoyu",
  age18,
  height1.88
}

var { name, age, height } = obj
console.log(name, age, height)

因为对象是可以通过key来解构的,所以它对顺序、个数都没有要求:

如果我们对变量的名称不是很满意,那么我们可以重新命名:

1
2
var { name: whyName, age: whyAge } = obj
console.log(whyName, whyAge)

我们也可以给变量一个默认值:

1
2
var { name, address = "广州市" } = obj
console.log(name, address)