图解面向对象——组团聊一聊JavaScript的new Object()

322 阅读29分钟

基础不牢,地动山摇。

对象很丰满,现实很骨感

对象可以将多个相关联的数据封装到一起,更好的描述一个事务:

classDiagram
Car <|-- 略
Car : +String color
Car : +Number speed
Car : +Number price
Car : +String brand
Car: +travel()

classDiagram
Person <|-- 略
Person : +String name
Person : +Number age
Person : +Number height
Person : +String eat
Person: +eat()
Person: +run()

对象来描述事务,更有利于我们将现实的事务,抽离成代码中的某个数据结构

有一些编程语言就是纯面向对象的编程语言,比如:Java,是实现抽象时先创建一个类,在根据类去创建对象;

JavaScript的面向对象

JavaScript包括函数式编程(FP)和面向对象编程(OOP):

在这里就不详细解释FP了, 一句话概括就是:函数是一等公民,执行过程分解成一系列可复用函数的调用。

JavaScript中的对象被设计成一组属性的无序集合,类似哈希表,由Key和Value组成;

key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型;

如果value是一个函数,就可以说是对象的方法。

封装

如何创建对象?

  1. 使用Object类,通过new关键字来创建对象:
let obj1 = new Object();
obj1.name = 'ThunderChen';
obj1.age = 18;
obj1.run = function () {console.log(this.name + "在跑步!")};
  1. 使用对象字面量
let obj2 = {
  name: 'ThunderChen',
  status() {
    console.log(this.name + "正在骑行回家!")
  }
};

对象属性的操作

基础

增删改查,直接上代码块:

let obj = {
  name: 'thunder',
  age: 18,
};
//获取属性
console.log(obj.name);
//给属性复制
obj.name = "kobe"
//删除属性
delete obj.name

控制

JavaScript对象通过属性描述符对属性进行比较精确的操作控制

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

可接收三个参数:

  • obj要定义属性的对象;
  • prop要定义或修改的属性的名称或 Symbol;
  • descriptor要定义或修改的属性描述符;
属性描述符分类

属性描述符的类型有两种:

  • 数据属性(Data Properties)描述符;
  • 存取属性(Accrssor访问器Properties)描述符;
configurableenumerablevaluewritablegetset
数据描述符可以可以可以可以不可以不可以
存取描述符可以可以不可以不可以可以可以
数据属性描述符

数据属性描述符有如下四个特性:

  • [[Configurable]] - 可配置的:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;

    • 字面量的形式定义属性时,默认值为true
    • 通过defineProperty定义一个属性时,默认值为false
  • [[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性;

    • 字面量的形式定义属性时,默认值为true
    • 通过defineProperty定义一个属性时,默认值为false
  • [[Writable]]:表示是否可以修改属性的值;

    • 字面量的形式定义属性时,默认值为true
    • 通过defineProperty定义一个属性时,默认值为false
  • [[value]]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改;

    • 默认情况下这个值是undefined;


Object.defineProperty(obj, 'address', {
  //很多配置
  value: '北京市', //默认值是undefined
  configurable: false, //不可配置, 不可以重新定义描述符 //默认值是false
  enumerable: false, //设置是否可以枚举的 //默认值是fasle
  writable: false, //该特性是属性是否可以赋值(写入) 默认值是false
});

存取属性描述符

存储属性描述符描述符:

  • [[Configurable]]:同属性描述符

  • [[Enumerable]:同属性描述符

  • [[get]]:获取属性时会执行的函数。默认为undefined

  • [[set]]:设置属性时会执行的函数。默认为undefined

let obj = {
  name: 'thunder',
  age: 18,
  _address: '北京市',
};
//存储属性描述符
//1. 隐藏某一个私有属性希望直接被外界使用和赋值
//2. 如果我们希望获取一个属性访问和设置值的过程时,也会使用存储属性描述符
Object.defineProperty(obj, 'address', {
  enumerable: true,
  configurable: true,
  get: function () {
    foo();
    return this._address;
  },
  set: function (value) {
    bar();
    this._address = value;
  },
});

console.log(obj.address);

obj.address = '上海市';

console.log(obj.address);

function foo(params) {
  console.log('获取一次address的值');
}
function bar() {
  console.log('设置了一次address的值');
}

其他

定义多个属性描述符

Object.defineProperties()

let obj = {
    _age :18,

};
Object.defineProperties(obj, {
  name: {
    enumerable: true,
    configurable: true,
    writable: true,
    value: 'thunder',
  },
  age: {
    configurable: true, //同顶部
    enumerable: true,
    get: function () {
      return this._age;
    },
    set: function (value) {
      this._age = value;
    },
  },
});

获取对象的属性描述符

Object.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptors()

let obj = {
    _age :18,
};
Object.defineProperties(obj, {
  name: {
    enumerable: true,
    configurable: true,
    writable: true,
    value: 'thunder',
  },
  age: {
    configurable: true, //同顶部
    enumerable: true,
    get: function () {
      return this._age;
    },
    set: function (value) {
      this._age = value;
    },
  },
});

//获取某一个特定属性的属性描述符
console.log(Object.getOwnPropertyDescriptor(obj,"name"));
//所有属性的描述符
console.log(Object.getOwnPropertyDescriptors(obj))

禁止对象扩展新属性

Object.preventExtensions()

var obj = {
  name: 'thunder',
  age: 18,
};
obj.height = 1.88;
obj.address = '北京市';
//.禁止对象添加新的属性
Object.preventExtensions(obj);
obj.hobby = '打游戏';
console.log(obj); 
//{ name: 'thunder', age: 18, height: 1.88, address: '北京市' }
密封对象,不允许配置和删除属性

Object.seal()

//禁止对象配置/ 删除里面的属性
Object.seal(obj);
delete obj.name;
//{name: 'thunder',age: 18,height: 1.88,address: '北京市',hobby: '打游戏'}
冻结对象,不允许修改现有属性(等价于writeable:false)

Object.freeze()

//让对象不可以修改(writeableL:false)
Object.freeze(obj);
obj.name = 'kobe';
console.log(obj);

//{name: 'thunder',age: 18,height: 1.88,address: '北京市',hobby: '打游戏'}

JS创建多个对象的方案

前面已经看到了字面量new Object方式去创建对象

这种方式弊端就是:创建同样的对象时,需要编写重复的代码

工厂模式

工厂模式大家都很熟悉,通过一个工厂的方法,产出我们想要的对象

function createPerson(name, age, height, address) {
  let p = {};
  p.name = name;
  p.age = age;
  p.height = height;
  p.address = address;
  p.run = function() {
    console.log(this.name + "正在跑步");
  }

  return p;
}

let p1 = createPerson('张三', 18, 1.88, '广州市');

let p2 = createPerson("李四",18, 1.88, '广州市');

let p3 = createPerson("王五",18, 1.88, '广州市');

//缺点
//获取不到对象的最真是的类型
console.log(p1,p2,p3);

打印 p1 、 p2 、 p3 对象的类型都是Object类型

构造函数

什么是构造函数(constructor)?

它也是一个普通的函数,如果它用new 操作符来调用的了,那个这个函数就称之为是一个构造函数

function foo (){
console.log('foo');
}

//foo是一个普通的函数
foo()
// 通过new关键字调佣foo函数, 那么这个函数就是一个构造函数了
new foo()

new foo

let f1 = new foo
console.log(f1);
new 操作符

那么通过new 操作符去调用一个函数时,发生了什么?(面试偶尔会问到)

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

过程如下:

function foo (name){
console.log('foo');
let newobj = {}
newobj.__proto__ = foo.prototype
this = newobj

//执行foo函数中的代码
this.name = name 

}

new foo('thunder')

因为我们可以通过构造函数实现:

function Person(name,age,height,address) {

    this.name = name
    this.age = age
    this.height = height;
    this.address = address
    
}

let p1 =  new Person("thunder",18,1.88,"北京市")

上面createPerson 和 Person 基本是一样的,只是有如下区别:

  • 没有显式地创建对象。
  • 属性和方法直接赋值给了 this。
  • 没有 return。

当前构造函数也有自己的缺点:

在创建对象时,我们需要为每个对象的函数去创建一个函数对象的实例

认识对象的原型

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

[[prototype]]的作用

  • 当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作;
  • 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它;
  • 如果对象中没有该属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;

问:那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢? 答案:是有的,只要是对象都会有这样的一个内置属性;

获取的方式有两种:

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

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

let obj = {name:"thunder"} //[[prototype]]

console.log(obj.__proto__)

let obj2 = {name:"thuner",__proto__:{}}

console.log(Object.getPrototypeOf(obj))

//[Object: null prototype] {}
//[Object: null prototype] {}

obj.__proto__.age = 12
console.log(obj.age)
//12

函数对象的原型prototype

所有的函数都有一个prototype的属性,被称为显示原型, 但是函数也是一个对象(new Function)它也是有[[prototype]],被称为隐士原型

function foo() {}

//函数也是一个对象
//函数作为对象来说,它也是有[[prototype]] 隐士原型
console.log(foo.__proto__);
//函数 也是一个函数, 所以他会多出来一个显示原型属性, prototype
console.log(foo.prototype);

我们再看new操作符

function Foo () {}
ler f1 = new Foo()

首先在内存中会创建一个新的对象(空对象) p = {} 接着对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性p.__proto__ = Foo.prototype

这就是说明通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype


let f1 = new foo
let f2 = new foo
cosole.log(f1.__proto__ === foo.prototype) //true
console.log(f2.__proto__ === foo.prototype) //true

创建对象的内存表现

创建对象时

function Person() {
    
}

let p1 = new Person
let p2 = new Person

添加属性时

p1.__proto__.name = "curry"

或者

Person.prototype.name = "clearlove"

或者

p1.name = "clearlove"

实际上是在Person函数中添加了 name属性

批量赋值新的对象

foo.prototype 中有一个constructor属性,我们先看一下prototype.constructor,

function foo() {}
console.log(foo.prototype);
console.log(Object.getOwnPropertyDescriptors(foo.prototype));

/*
    {}
    {
      constructor: {
        value: [Function: foo],
        writable: true,
        enumerable: false,
        configurable: true
      }
    }
*/
console.log(foo.prototype.constructor); //[Function: foo]
console.log(foo.prototype.constructor.name); //函数名字

如果需要在原型上添加过多的属性,通常我们会重新整个原型对象:

function Foo() {}
Foo.prototype = {
  big: 18,
  long: 18,
};

每创建一个函数, 就会同时创建它的prototype对象, 这个对象也会自动获取constructor属性,这里相当于给了prototype重新赋值了一个对象,

现在需要重新将constructor指向Person:

   Foo.prototype = {
       constructor:Foo,
       big: 18,
       long: 18,
    };

上面的方式虽然可以, 但是也会造成constructor的[[Enumerable]]特性被设置了true.默认情况下, 原生的constructor属性是不可枚举的.

这时需要Object.defineProperty()

//真实开发中我们可以通过Object.defineProperty 方法添加constructor
Object.defineProperty(foo.prototype, 'constructor', {
  enumerable: false,
  configurable: true,
  writable: true,
  value: Foo,
});

创建对象 – 构造函数和原型组合

个构造函数的方式创建对象时,有一个弊端:会创建出重复的函数,比如running

因此只需要将函数放到Person.prototype的对象上即可

function Person(name, age, heigh, address) {
    this.name = name
    this.age = age 
    this.heigh = heigh
    this.address  = address
}

Person.prototype.eating = function() {
    console.log(this.name + "在吃饭!");
}

let p1 = new Person('why', 18, 1.22, '北京市');
let p2 = new Person('thunder', 18, 1.22, '北京市');

面向对象的特性

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

封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;

继承:继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);

多态:不同的对象在执行时表现出不同的形态;

JavaScript原型链

let obj = {
    name:"thudner",
    age:18
}
obj.__proto__ = {}

obj.__proto__.__proto__ = {}

obj.__proto__.__proto__.__proto__ = {
    address:"北京市"
}
console.log(obj.address); 
// 北京市

当我们用点语法引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作;在当前对象中查找属性, 如果没有找到, 这个时候回去原型(__proto__)对象上查找,如下图:

Object的顶层原理

我们知道如果我们这样测试:obj.__proto__.__proto__发现为 null,可以明确的知道,原型链是有尽头的

那么到底是找到那一层对象之后就停止继续查找了呢?

通过字面量创建的obj的原型是obj.__proto__ => [Object: null prototype] {},obj.__proto__.__proto__ => null, [Object: null prototype] {}就是该对象的顶层原型。

需要注意的是:

[Object: null prototype] {} 原型是一个特殊的对象,1. 该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型; 2.该对象上有很多默认的属性和方法

如图所示:

打印Object Object.getOwnPropertyDescriptors(Object)

{
  length: { value: 1, writable: false, enumerable: false, configurable: true },
  name: {
    value: 'Object',
    writable: false,
    enumerable: false,
    configurable: true
  },
  prototype: {
    value: [Object: null prototype] {},
    writable: false,
    enumerable: false,
    configurable: false
  },
  assign: {
    value: [Function: assign],
    writable: true,
    enumerable: false,
    configurable: true
  },
  getOwnPropertyDescriptor: {
    value: [Function: getOwnPropertyDescriptor],
    writable: true,
    enumerable: false,
    configurable: true
  },
  getOwnPropertyDescriptors: {
    value: [Function: getOwnPropertyDescriptors],
    writable: true,
    enumerable: false,
    configurable: true
  },
  getOwnPropertyNames: {
    value: [Function: getOwnPropertyNames],
    writable: true,
    enumerable: false,
    configurable: true
  },
    ...
}

顶层原型来自哪里?

前面我们回顾到,通过构造函数的形式创建一个对象时:

let obj = new Object()
  1. let linshi = {}
  2. this = linshi
  3. 将函数的显示原型prototype赋值给前面创建出来的对象的隐士原型 linshi.__proto__ = obj.prototype
  4. ...
  5. 将linshi返回

此时 obj.__proto__ = Object.prototype

再看一个案例

let obj = {
  name: 'thunder',
  age: 18,
};

let obj2 ={
    address:"北京市"
}
obj.__proto__ = obj2

得到的原型图如下:

函数的原型

function Person(name,age) {
    this.name = name
    this.age = age
}
console.log(Person.prototype); // {}
console.log(Object.getOwnPropertyDescriptors(Person.prototype))
let p1 = new Person('thuhder',18)
console.log(Person.prototype.__proto__)
console.log(Person.prototype.__proto__.__proto__)

打印结果如下:

1. {}
2. {
  constructor: {
    value: [Function: Person],
    writable: true,
    enumerable: false,
    configurable: true
  }
}
3. [Object: null prototype] {}
4. null

通过打印的1和2可知,Person的显示原型中是有Constructor对象的,通过3可知Person的显示对象中含有隐士原型. 我们可以得到原型图如下:

调试细节

看这样几行代码:

<body>
    <script>
      let obj = {
        name: 'thunder',
        age: 18,
      };
      Object.defineProperty(obj, 'address', {
        value: '北京市',
      });
      console.log(obj);
    </script>
  </body>

通过node直接运行JS代码部分结果如下: { name: 'thunder', age: 18 }并没有address

F12打开浏览器调试发现:

address字段是灰色的

浏览器是为了方便开发者调试,灰色代表改属性是不可枚举的(Enumerable:false).

继承

我们先来看一段代码:

function Student(name,age,sno) {
this.name = name
this.age = age 
this.sno = sno
}
Student.prototype.running = function (params) {
    console.log(this.name + "在跑步!");
}
Student.prototype.eating = function (params) {
    console.log(this.name + "在跑步!");
}
Student.prototype.studying = function (params) {
    console.log(this.name + "在跑步!");
}
function Teacher(name,age,title) {
    this.mame = name
    this.age = age
    this.title = title
}
Teacher.prototype.running = function (params) {
    console.log(this.name + "在跑步!");
}
Teacher.prototype.eating = function (params) {
    console.log(this.name + "在跑步!");
}
Teacher.prototype.teaching = function () {
    console.log(this.name + "在讲课!");
}

Student 和 Teacher 多非常多相同的属性和方法,我们需要单独的抽取到一个独立的位置,每个对象只需要写自己特有的属性和方法。抽取完毕后当我们new Student的时候,该对象必须还有原来的所有属性和方法。这时候需要面向对象中的继承

继承中,把公共的属性和方法抽取到父类中,对于Teacher, Student 相当于是子类,存储自己特有的属性和方法,只需子类继承父类

通过原型链继承

代码如下:

//父类:公共属性和方法
function Person() {
    this.name = "thunder",
    this.friends = []
}
Person.prototype.eating = function () {
    console.log(this.name + "eating~");
}
//子类:特有的属性和方法
function Student(params) {
    this.sno = 111
}
let p = new Person()
Student.prototype = p
Student.prototype.studying = function (params) {
    console.log(this.name + "studying~");
}
let stu = new Student()
console.log(stu.name);
console.log(stu.eating());

需要注意的是:Student.prototype = p; Student.prototype.studying... 两行代码不能调换位置,不然会导继承失败,因为Student的显示原型指向P对象,之后 .studying其实是在 p 对象中添加了一个Studying的方法,最后new Student 创建一个stu对象,此时它也是指向P对象的,所以调用stu.eating();是没有问题的如下图:

(stu2 / Student原型对象 都或被垃圾回收(GC))

原型链的继承三大弊端

  1. 打印对象(上述stu),某些属性是看不到的
  2. 其中的属性会别多个对象共享,直接修改对象上的属性,是给对象本省添加了一个属性如果获取引用,修改引用中的值,会互相影响
let stu1 = new Student()
let stu2 = new Student()
console.log(stu1.friends); // []
console.log(stu2.friends);// []
stu1.name = "kobe"  
console.log(Object.hasOwn(stu1,'name')); //true
console.log(stu2.name);//thunder
console.log(Object.hasOwn(stu2,'name')); //false
stu1.friends.push("thunder")
console.log(stu1.friends);//[ 'thunder' ]
console.log(stu2.friends);//[ 'thunder' ]
  1. 不能给Person传递参数,因为这个对象是一次性创建的(没办法定制化);在前面实现类的过程中都没有传递参数, 打比方我们这样传递参数,上代码:
let stu3 = new Student("thunder",18)
//对于Student的参数需要满足共享和自足
//共享就是有些参数是需要存储到new Person对象中;自足就是该参数值是new Student对象中使用
//很显然 Student 函数无法实现,更不能想Person函数中传递参数了
function Student(name,age) {
    this.sno = 111
    this.name = name
    this.age = age
}

借用构造函数继承

为了解决上面三个问题,前辈们又想出了一种好的办法:constructor stealing(有很多名称: 借用构造函 数或者称之为经典继承或者称之为伪造对象)

构造函数继承:在子类型构造函数的内部调用父类型构造函数.

父类:包含公共属性和方法

子类:特有的(自身)属性和方法

构造函数继承:

function Person(name,friends,age){ 
    (this.name = name), (this.friends = friends);
  this.age = age;
   } 
   Person.prototype.addFunc = function() {
    console.log(12323);
   }
   function Student(name, age, friends) { 
    Person.call(this, name, age, friends); //借用构造函数
    // 实例属性
    this.sno = 111;
   } 
   let stu = new Student('thunder', 12, ['thunderchen']);
   console.log(17,Object.getOwnPropertyDescriptors(stu));
   console.log(18,stu.hasOwnProperty('name'));
   console.log(stu.name); // "thunder"; 
   console.log(stu.age); // 12
   stu.addFunc() //TypeError: stu.addFunc is not a function


如图:

验证代码:

  console.log(17,Object.getOwnPropertyDescriptors(instance));
  console.log(18,instance.hasOwnProperty('name'));
 /* 17 {
  name: {
    value: 'thunder',        
    writable: true,
    enumerable: true,        
    configurable: true       
  },
  friends: {
    value: [ 'thunderchen' ],
    writable: true,
    enumerable: true,        
    configurable: true       
  },
  age: { value: 12, writable: true, enumerable: true, configurable: true },
  sno: { value: 111, writable: true, enumerable: true, configurable: true }
}
18 true
*/

在我们new Student()的时候,通过call执行Person函数,此时Person => this => new Student(stu)

通过Call方法显示的绑定this,其实就已经解决了上述的三个问题:

  1. 显示自身的所有属性
console.log(stu)
//Person { name: 'thunder', friends: ['thunderchen'], sno: 111 }
  1. 共享对象,互不干扰
let stu1 = new Student('thudner', 18, ['why']);
let stu2 = new Student('thudner', 20, ['clearlove']);
stu1.friends.push("thunderchen")
console.log(stu1.friends);
console.log(stu2.friends);
//[ 'why', 'thunderchen' ]
//[ 'clearlove' ]
  1. 传递参数
let stu3 = new Student("thunder",18)

现在可以按照指定的需求进行传递了,Nice!

需要注意的是如果参数默认不传new Student(),默认值为undefined

借用构造函数继承弊端

必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法.

组合式继承

组合式继承是使用原型链继承的方式继承原型上的属性和方法,而通过借用构造函数继承的方式继承实例属性,

function Person(name, age, friends) {
  (this.name = name), (this.friends = friends);
  this.age = age;
}
Person.prototype.eating = function () {
  console.log(this.name + 'eating~');
};
function Student(name, age, friends) {
  Person.call(this, name, age, friends); //借用构造函数
  this.sno = 111;
}
let p = new Person(); //修改原型链
Student.prototype = p;
Student.prototype.studying = function (params) {
  console.log(this.name + 'studying~');
};
let stu = new Student('thunder', 12, ['thunderchen']);

结构图如下:

借用构造函数继承.png

组合继承弊端

  1. Person 函数(父类)至少会被调用两次

    • 一次在创建子类原型的时候;
    • 另一次在子类构造函数内部(也就是每次创建子类实例的时候);
  2. stu的原型上会多出一些属性,所有的子类实例事实上会拥有两份父类的属性,但是这些属性是没有存在的必要的,

    • 一份在当前的实例自己里面(也就是stu1本身的),另一份在子类对应的原型对象中(也就是 stu1.__proto__里面);

    • 这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的;

原型式继承

这种继承方式是Douglas Crockford 提出的,看代码:

let obj = {
  name: 'thudner',
  age: 18,
};
//方法1
function createObject(o) {
  let newObj = {};
  Object.setPrototypeOf(newObj, o);
  return newObj;
}
//方法2
function createObject2(o) {
  function Fn() {}
  Fn.prototype = o;
  let newObj = new Fn();
  return newObj;
}
//方法3
let info = Object.create(obj)
console.log(info);
console.log(info.__proto__);

他推荐:有一个对象,想在它的基础上再创建一个新对象。 你需要把这个对象先传给 公用函数createObject(),然后再对返回的对象进行适当修改。在这个例子中,obj对象定义了另一个对象也应该共享的信息,把它传给 createObject()之后会返回一个新对象。这个新对象的原型是obj,意味着它的原型上既有原始值属性又有引用值属性。这也意味着obj.name不仅是obj的属性,也会跟info共享。这里实际上克隆了两个obj。

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合

属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的:

let info = Object.create(obj)
info.age = 30
info.friends.push("clearlove")
console.log(info); //{ age: 30 }
console.log(info.__proto__);//{ name: 'thudner', age: 18, friends: [ 'clearlove' ] }
let info2 = Object.create(obj)
console.log(info2);//{}
console.log(info2.__proto__);//{ name: 'thudner', age: 18, friends: [ 'clearlove' ] }

寄生式继承

寄生式继承的思路类似上面的原型式继承和工厂函数:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。

代码如下:

let p = {
    running:function() {
        console.log("running");
    }
}
function createStudent(name) {
    let stu = Object.create(p)
    stu.name = name
    stu.studying = function(){
        console.log("isstudying")
    }
    return stu
}
let stuObj = createStudent("thunder")
console.log(stuObj.__proto__);

寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景

寄生式继承弊端

寄生式继承给对象添加函数会导致函数难以重用,上节中,创建多个对象的同时也会创建多个studying函数

寄生组合式继承

寄生式组合继承通过借用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型

先分析组合式继承的问题,构造函数被调用两次,父类原型中的属性会有两份。

那么我们怎么解决这两个问题呢?

答案:通过寄生式继承。

其次,我们还需要获取一份父类型的原型对象中的属性和方法,代码如下:

function inheritPrototype(SubType,SuperType) {
    SubType.prototype = Object.create(SuperType.prototype)
Object.defineProperty(SuperType.prototype,"constructor",{
    enumerable:false,
    configurable:true,
    writabel:true,
    value:SubType
})
}
function Person(name, age, friends) {
  this.name = name;
  this.age = age;
  this.friends = friends;
}
Person.prototype.running = function () {
  console.log('running');
};
Person.prototype.eating = function () {
  console.log('eating');
};
function Student(name, age, friends, sno, score) {
  Person.call(this, name, age, friends);
  this.sno = sno;
  this.score = score;
}
inheritPrototype(Student,Person)
Student.prototype.studying = function () {
  console.log('studying');
};

let stu = new Student("thudner",18,["thudner"],111,100)
console.log(stu);
stu.studying()

寄生组合式 = 寄生式 + 原型式 + 借用构造函数 + 工厂函数;红宝书:寄生式组合继承可以算是引用类型继承的最佳模式。

其他知识补充

hasOwnProperty

对象是否有某一个属于自己的属性(不是在原型上的属性)

let obj = {
  name: 'thunder',
  age: 18,
};

let info = Object.create(obj, {
  address: {
    value: '北京',
    enumerable: true,
  },
  name: {
    value: 13,
    enumerable: true,
  },
});
console.log(info.hasOwnProperty('address')); //true
console.log(info.hasOwnProperty('age'))//false

//in 操作符 :不管在当前点对象还是原型中都是true
console.log("address" in info);
//for in 也是同理
for (const key in info) {
    console.log(key);
}
// address
// name
// age

instanceof

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

function inheritPrototype(SubType,SuperType) {
    SubType.prototype = Object.create(SuperType.prototype)
Object.defineProperty(SuperType.prototype,"constructor",{
    enumerable:false,
    configurable:true,
    writabel:true,
    value:SubType
})
}
function Person() {
}
function Student() {
}
inheritPrototype(Student,Person)
let stu = new Student()
console.log(stu instanceof Student); //true
console.log(stu instanceof Person); //true
console.log(stu instanceof Object); //true

isprototypeOf

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

function Person() {}
let  p = new Person()
console.log(Person.prototype.isPrototypeOf(p)); //用于检测某个对象,是否出现在某个实例对象的原型链上
let obj  = {
    name:"thunder",
    age:18
}
let info = Object.create(obj)
console.log(obj.isPrototypeOf(info));

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

let obj = {
    name:"thunder"
}

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

对象中有一个__proto__对象,为隐士原型对象。

函数也是特殊的对象:

function Foo() {}
console.log(Foo)// {constructor:Foo}

Foo函数是一个函数,那么他会一个显示原型对象, Foo.prototype。

Foo.prototype来自哪里? 答:当函数声明创建一个函数时,Foo.prototype = {constructor:Foo} 这是JS引擎自动创建的

let Foo = new Function()

Foo 也是一个对象,那么他会有一个隐士原型对象: Foo.__proto__

Foo.__proto__ 来自哪里?

答:new Function() => Foo.__proto__ = Funciton.prototype, Funciton.prototype = {constructor = Function}

测试:

console.log(Foo.prototype === Foo.__proto__) //false
console.log(Foo.prototype.constructor); //[Function: Foo]
console.log(Foo.__proto__.constructor); //[Function: Function]

在这里我就不一一描述了,对照这张图自己讲述一遍,应该是没有问题的。

最后在来一张更全的:

注意:有两点特殊的地方1、function Object(){} 2、function Function(){}

特殊就在他们也是对象,都含有__proto__,因此他们的原型都指向Function.prototype

认识Class类

关于这一章,作为一个简单的总结,因为有更优秀的文章值得去浏览:阮一峰-ES6-Class

在ES6(ECMAScript2015)新的标准中使用了class关键字来直接定义类;Class是以构造函数的形式创建类的语法糖。

类的声明

定义类有两种方式:类声明类表达式

//类的声明
class Person {}
//类的表达式
let Animal = class {}

class类的方式和构造函数在原型链上的表现形式是一样的:

//类的特点
console.log(typeof Person); //function
console.log(Person.prototype);//{}
console.log(Person.prototype.__proto__);//[Object: null prototype] {}
console.log(Person.prototype.constructor);//[class Person] 
console.log(Person.__proto__);//{}
// function Person() {}
let p = new Person()
console.log(p.__proto__ === Person.prototype); //true

唯一的区别就是constructor描述为[class Person],不要在此纠结,你可以理解这是JS引擎为了更明确的描述原型的指向而添加的类型描述。

Class构造方法

同样Class类也有自己的构造函数,这个方法的名称是固定的constructor;当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor;每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常;


class Person {
    //类的构造方法
    //注意:一个类只能有一个构造函数
    constructor(name,age) {
        this.name = name
        this.age = age
    }
}
let p1 = new Person("thunder",18)
console.log(p1); //Person { name: 'thunder', age: 18 }

当然,constructor可以不写,如果没有显式定义,一个空的constructor()方法会被默认添加。

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

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

构造函数创建实例对象的过程是一样的。

类的方法

包含

  • 实例方法

    • 通过构造函数创建实例对象时我们通常会将方法放到函数的原型对象上,同理calss亦是如此,只需与构造器同级即可。
  • 访问器方法、

    • 对象的属性描述符可以添加setter和getter函数的,同理calss亦是如此。
  • 静态方法

    • 静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static关键字来定义。

代码如下:

let names = ["abac","thunder"]
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    this._address = '北京市';
  }
  //普通的实例方法
  //创建出来的对象进行访问
  // let p = new Person()
  eating() {
    console.log(this.name + 'eating~');
  }
  running() {
    console.log(this.name + 'running~');
  }
  //类的访问器
  get address() {
    return this._address;
  }
  set address(newAddress) {
    this._address = newAddress;
  }
// 同 => 
// let obj = {
//   _name: 'thunder',
//   get name() {
//     return this._name;
//   },
//   set name(newValue) {
//     this._name = newValue;
//   },
// };

  //类的静态方法(类方法)
  // Person.randomPerson()
  static randomPerson() {
    let nameIndex =Math.floor(Math.random() * names.length)
    let name = names[nameIndex]
    return new Person(name)
  }
}

类的继承 - extends

上面我们学到了ES5中实现继承的方案,虽然最终实现了相对满意的继承机制,但是过程却依然是非常繁琐的,在ES6中新增了使用extends关键字,可以方便继承:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

//Student被称之为子类(派生类)
class Student extends Person {
    constructor() {
    ...
   }
}
let stu = new Student()

需要注意的是,上面代码你直接运行的话,是会报错的:ReferenceError: Must call super constructor in derived ...。显示的声明constructor必须调用super方法,此时我们console.log(stu) // Student { name: undefined, age: undefined },接着我们去掉子类的constructor方法再次打印stu,得到的结果是一样的,因此我们可以推断出,子类中未显示定义构造器会默认调用以及super方法。

接下来就是如何继承父类中的属性,并且如何赋值的问题了。

类的super关键字

在子(派生)类的构造函数中使用this或者返回默认对象之前,必须先通过super调用父类的构造函数!

JS引擎在解析子类的时候要求我们有实现继承那么子类的构造方法中,在使用this之前或者constructor在返return this之前调,必须调用super方法。

调用形式:

  1. super([arguments]) => super()
  2. super.functionOfParent([arguments]) => super["父类的方法"]()
...
class Student extends Person {
  constructor(name, age, sno) {
    super(name, age);
    this.sno = sno;
  }
}

将name 和age传递过去,交给父类处理,赋值给父类(this)的对象里面,我们打印Stu 发现可以显示name age属性,因此创建的属性还是在student创建出来的对象中。其实表现的形式类似借用构造函数继承中的call操作

super的使用位置有三个:子类的构造函数、实例方法、静态方法;

子类的构造函数上面已经提到过了, 我们来看实例方法和静态方法

  1. 实例方法

在父类和子类中定义各自实例方法:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
 running() {
    console.log(this.name + 'running!');
  }
}
class Student extends Person {
  constructor(name, age) {
    super(name,age);
  }
  studying() {
    console.log(super.running());
  }
}
let stu = new Student();

子类调用父类中的方法需要通过super关键字去调用也就是调用方式第二点。

当然也可以在子类中定义父类中的方法,这种方式叫做方法的重写

class Student extends Person{
    running() {
        ...
    }
}

我们还可以对子类的方法进行扩展:

class Student extends Person{
    running() {
        super.running()
        console.log(777,'do something')
    }
}
  1. 静态方法

静态方法前面标有static关键字,只能通过类的形式去调用。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
 static running() {
    console.log(this.name + 'running!');
  }
}

class Student extends Person {
  constructor(name, age) {
    super(name,age);
  } 
  static running() {
    super.running()
    //do something
  }
}
let stu = new Student();

子类可以继承父类的静态方法:Student.running()

子类也可以重写父类的静态方法:Student.running()

Class转ES5

ES6转ES5我们可以借助babel转码工具(链接)

找到左侧TARGETS

修改not ie 11 => not ie 10

基本转换

源代码:

class Person{}

babel转换后的代码:

我们先重点分析:_classCallCheck(_ccc)函数和Person函数

直接调用Person(),执行_createClass(_cc)函数,默认返回 function Person(){} , 执行_ccc函数此时this指向的是window对象,很显然window对象不在Person函数的原型中,因此if (!(instance instanceof Constructor)) => false 抛出异常,可见_ccc是限制Person函数的调用,必须通过new方式创建实例对象。

属性和方法

我们在类中添加属性和方法

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  running() {
    console.log(this.name + 'running!');
  }
}

转换后的代码如下:

_ccc函数_cc函数以及_defineProperties(_dfp)函数同上

Person是一个立即执行函数,new Person的时候会自动执行该函数并且返回Perosn.

那么为什么Person是一个立即执行函数(IIFE)呢?

立即执行函数有形成自己的函数作用域,不受全局的干扰,因此内部的变量/函数不会于外界冲突。其次IIFE前面有/*#__PURE__*/,一般称为魔法注释,将次函数标记为“纯函数”,纯函数在这里不详细解释,目的是为了一些打包工具例如webpack将文件压缩处理时方便tree-shaking,消除未调用的代码。

在上面ES5继承中定义方法是:Person.prototype.running = function(){}, 它是通过_cc函数将running作为第二个参数,通过_dfp函数添加方法。

protoProps => true;

Constructor.prototype => Person原型,protoProps => [{key:string,value:function}];

_dfp函数遍历protoProps,通过defineProperty将方法添加到Peroson.prototype中。

其实还是蛮简单的

继承转换

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  running() {
    console.log(this.name + 'running!');
  }
}
class Student extends Person {
  constructor(name, age) {
    super(name,age);
  }
  studying() {
    console.log(super.running());
  }
}

经过babel转换后:

193行代码你没有看错,有点想放弃了。

这里解释一下比较重要的代码:

Student函数亦是IIFE

先分析_inherits(_iht)函数,_int限制父类必须是函数,下面的代码是不是看着很熟悉?没错就是寄生组合式继承中的inheritPrototype函数,将父类中的原型方法赋值到子类的原型。_setPrototypeOf(_spto)函数,是将o.__proto__ = p; => Student.__proto__ = Person,注意和上面的继承函数区分开,它的目的继承父类的静态方法,举个例子:

calss Person {
 static running() {
    console.log(this.name + 'running!');
  }
}
...

假如子类想直接调用父类的静态方法,Student.running()很明显是访问不到的,此时会沿着原型链Student.__proto__去找,但是此时它指向的Function.prototype,那么直接修改Student的原型链指向Person可以获取到相关的静态方法。

接下来在分析_createSuper(_cs)函数

 var _super = _createSuper(Student);
 ...
 _super.call(this, name, age);

这两行代码看着是不是眼熟?没错它就是类似借用构造函数继承中的call方法,区别就是通过_cs函数得到了新的函数,在上面提到过_ccc函数是限制Person函数的调用,必须通过new方式创建实例对象。

_createSuper(_cs)函数首先判断是否支持Reflect Api ,_getPrototypeOf(Derived => Student)获取的是Student的父类,返回的是Person函数。如果支持Reflect,会创建一个新的Student函数,在通过Reflect.construct创建一个新的Student对象并且原型指向Person。

Reflect.construct(Super,argument,newTarget):通过Super创建出来一个实例,但是这个实例的原型Constructor指向的是NewTarget => 会通过Person创建出来一个实例,但是这个实例的原型constructor指向的Person

_possibleConstructorReturn 还是将result返回

最终得到的是:

接下来_this = _super.call(this,name,age)得到的对象为{name:xxx,age:xxx},最后将this返回,赋值给stu实例对象。

多态

什么是多态?

多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型 —— 维基百科

简单的说就是:不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现。

传统意义上的多态有三个前提:

  1. 必须是继承(是多态的前提)
  2. 必须有重写(子类重写父类的方法)
  3. 必须有父类引用指向子类的对象

代码如下(TS ):

class Shape {
  getArea() {}
}
class Rectangle extends Shape {
  getArea() {
    return 100;
  }
}
class Circle extends Shape {
  getArea() {
    return 200;
  }
}
let r = new Rectangle();
let c = new Circle();
function calcArea(shape: Shape) {
    shape.getArea()
}
calcArea(r);
calcArea(c);

传入不同的实例对象,调用相同的方法,获取到的结果是不一样的。

JavaScript中的多态,道理也是一样的, 直接看一看代码:

function calcArea(foo) {
  console.log(foo.getArea())
}
var obj1 = {
  name: "thudner",
  getArea: function() {
    return 1000
  }
}
class Person {
  getArea() {
    return 100
  }
}
var p = new Person()
calcArea(obj1)
calcArea(p)
// 也是多态的体现
function sum(m, n) {
  return m + n
}
sum(20, 30)
sum("abc", "cba")

由于JS的是一门动态语言,通过表现行是来看一定存在多态的。

完结

我们从对象、原型链、继承、ES6Class、代码转换等来介绍面向对象相关知识点,函数式编程(FP)和面向对象编程(OOP)都有各自的优势,我个人是对面向对象编程是有一些抵触的,但是每次new一个对象的时候有充满的无限的神秘感,我相信这是一场美丽的邂逅。

此篇文章献给我的三位同事(CC,YQ,YHY),感谢一年来的鼓励和帮助。祝你们未来可期,前程似锦。