JS 面向对象

吐槽君 分类:javascript

面向对象

最近在复习 javascript 基础知识,特此给自己记录下来,一次搞定对象

  • 类:封装、多态、继承
  • 构造函数、实例、对象字面量
  • 命名空间
  • 内置对象、宿主对象、本地对象

首先 JS 中没有类,都是基于原型的。无论是 ES5/ES6 中引入的 class 只是基于原型继承模型的语法糖。

image.png

构造函数

本身就是一个函数,为了规范一般将首字母大写,区别在于 使用 new 生成实例的函数就是构造函数,直接调用的就是 普通函数。

// ********** 手写 new 的几个步骤 ********** //
// 1、创建一个新对象,
// 2、把 构造函数的 prototype 赋值给新对象的 proto,
// 3、把构造函数的 this 指向新对象并返回结果
// 4、判断如果结果是对象则 返回 ,否则返回 新对象

function myNew(fn, ...args) {
  let newobj = {};
  newobj.__proto__ = fn.prototype;
  let resObj = fn.apply(newobj, args);
  // 判断如果结果是对象则 返回 ,否则返回 新对象
  return resObj instanceof Object ? resObj : newobj;
}
// 测试
function Parsen() {
  this.name = "龙哥";
  this.age = "18";
}
Parsen.prototype.getName = function () {
  return this.name;
};
var parsen = myNew(Parsen);
 

原型和原型链

原型模式

原型是在构造函数中的
每声明一个函数的时候:
浏览器会在内存中创建一个对象,对象中新增一个 constructor  属性,浏览器把 constructor  属性指向 构造函数,构造函数.prototype 赋值给对象。

Javascript 对象从原型继承方法和属性,而Object.prototype在继承链的顶部。Javascript prototype 关键字还可以用于向构造函数添加新值和方法。

原型链:
代码读取某个属性的时候,首先在实例中找到了则返回,如果没有找到,则继续在 实例的原型对象(proto)中搜索,直到找到为止。还没找到则继续 原型对象的原型对象上找。 直到最后 Object 为止,返回 null

关系

  • prototype:函数的一个属性:是一个对象 {}
  • proto:是对象 Object 的一个属性:对象 {}
  • **每个对象实例都有一个 __**proto__  ,它指向构造函数的 prototype
  • 以上关系可以使用 console.log 去测试
function Test() {
  this.a = 1;
};
var t = new Test();

// 因为 对象的 __proto__ 保存着该对象构造函数的 prototype 所以
t.__proto__ === Test.prototype // true

Test.prototype.__proto__ === Object.prototype // true: 这样一个链式调用形成 - 原形链

Object.prototype.__proto__ // null  原形链顶层为 null

/************* 原形链 *****************/

Test.prototype.b = 2; // 给构造函数的原型添加一个 属性b=2
Object.prototype.c = 3
console.log(test)
// 画一下这个关系链:
// test:
// {
//     a: 1,
//     __proto__: Test.prototype = {
//         b: 2,
//         __proto__: Oject.prototype = {
//         c:3
//         没有 __proto__
//         }
//     }
// }
// 原形链: 沿着__proto__为节点去找构造函数prototype 连起来的一个链条, 一层一层往上寻找 直到 null

console.log(test.constructor) // Test(){}
其实 test.constructor 指向的就是 实例化 test 对象的构造函数
所以:constructor 是可以被赋值修改的,


/************* 特殊性 *****************/
FunctionObject:函数 对象
Test.__proto__ === Function.prototype // true
Function.__proto__ === Function.prototype // true  因为函数自己构造了自己
Object.__proto__ === Function.prototype // true

/************* 属性的查找 *****************/
// test => {a: 1, b:2}
test.hasOwnProperty('a') // true    查找当前对象上的原型属性
test.hasOwnProperty('b') // true
test.hasOwnProperty('c') // false    是继承过来的所以没有

'a' in test // true   in: 链上查找
 

Class

/************ ES5 定义类 ******************/
function User(name) {
  this.name = name;
}
// 添加函数
User.prototype.showUser = function () {
  console.log(this.name);
};
// 使用
const user = new User("my.yang");
user.showUser();

/************ ES6 定义类 ******************/
class Person {
  // 其实就是在 Person 的 prototype 上添加了属性和方法
  constructor(name) {
    this.name = name;
  }
  showName() {
    console.log(this.name);
  }
}

const person = new Person("My.Yang");
person.showName();

/******************* 类的实例 ***************************/

// 实例的属性除了 this 定义在本身,其他都是定义在原型上
console.log(person.hasOwnProperty("name")); // true
console.log(person.hasOwnProperty("showName")); // false
console.log(person.__proto__.hasOwnProperty("showName")); // true

// 与 ES5 一样,类的所有实例共享一个原型对象。
var a1 = new Person("long");
var a2 = new Person("mmmmm");
console.log(a1.__proto__ === a2.__proto__);

// 所以不推荐直接通过 __proto__ 添加私有属性和方法,因为 共享的实例都会受到影响

/****************注意点:********************/
// 1、类和模块的内部默认使用严格模式
// 2、不存在变量提升  提前访问会报错 ReferenceError

// 静态方法:static
// 添加 static 关键字,表示该方法不会被【实例】继承,只能通过类.直接调用,但是可以被 子类继承 extends

// 私有方法和属性
// 私有方法:通过内部使用 _xxx、_func 的方式来定义
 

1、创建了一个 名为 User 的函数,该函数将成为类声明的结果
2、在 User.prototype 中存储所有方法,例如 showUser ( 跟 ES5 一样把函数存到 prototype 上 )
3、类必须使用 new ,否则无法调用类构造函数 报错
4、类方法 是不可以枚举的

继承

Class 使用 extends实现继承,比 ES5 通过修改原型链实现继承,要清晰和方便很多。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }
  toString() {
    return this.color + " " + super.toString(); // 调用父类的toString()
  }
}
 

注意:
子类必须在 constructor 中先调用 super(),否则新建实例就会报错。ReferenceError
主要是 解决 this 问题,需要先把 父类属性和方法加到 this 上,然后再用子类构造函数修改 this 。

使用 Object.getPrototypeOf() 判断 类是否继承了另一个类
Object.getPrototypeOf(Xxxx) === XXX

super 既可以当作函数又可以是对象
当作函数: super() 代表 父类的构造函数。 => A.prototype.constructor.call(this)

当作对象:super 在普通方法中,指向 父类的原型对象 A.prototype;在静态方法中 指向父类。

1、原型链继承

直接让子类的原型对象 prototype 指向父类实例,当子类实例找不到对应的属性和方法时,就会往它的原型对象(父类实例上找),从而实现对父类的属性和方法的继承。

// 父类
function Parent() {
  this.name = "我是小样";
}
// 给父类添加方法
Parent.prototype.getName = function () {
  return this.name;
};
// 子类
function Child() {}

Child.prototype = new Parent(); // 直接把父类实例赋给 子类的 原型对象
Child.prototype.constructor = Child; // 根据原型链的规则,顺便绑定一下constructor, 这一步不影响继承, 只是在用到constructor时会需要

const child = new Child();
child.name; // 我是小样
child.getName(); // 我是小样
 

缺点:【子类相互影响】 子类实例原型都指向父类实例,因此 某个 子类实例修改了 父类引用方法或函数的时候 会影响所有子类。 同时无法向父类构造函数传参

2、构造函数继承

在子类构造函数中执行 父类构造函数并绑定子类 this, 使得 父类中的属性能够赋值到子类的 this 上。这样就 避免实例之间共享一个原型实例,又能向父类构造函数传参。
缺点很明显: 继承不了父类原型上的属性和方法

function Parent(name) {
  this.name = [name];
}
Parent.prototype.getName = function () {
  return this.name;
};
function Child() {
  Parent.call(this, "参数1"); // 执行父类构造方法并绑定子类的this, 使得父类中的属性能够赋到子类的this上
}

//测试
const child1 = new Child();
const child2 = new Child();
child1.name[0] = "foo";
console.log(child1.name); // ['foo']
console.log(child2.name); // ['zhangsan']
child2.getName(); // 报错,找不到getName(), 构造函数继承的方式 继承不到父类原型上的属性和方法
 

3、组合式继承

说白了就是 把上面两个整合在一起, prototype、构造函数调用父类 call

function Parent(name) {
  this.name = [name];
}
Parent.prototype.getName = function () {
  return this.name;
};

function Child() {
  Parent.call(this, "参数"); // 改变 Parent this 指向,并带参数
}
Child.prototype = new Parent(); // 把父类实例 赋值给 子类原型
Child.prototype.constructor = Child;

var child = new Child();
var child2 = new Child();
child1.name = "yang";
console.log(child1.name); // yang
console.log(child2.name); // 参数
child2.getName(); // 参数
 

缺点: 每次创建子类实例都执行了两次构造函数【Parent.call() 和 new Parent()】,导致子类创建实例时,原型中会存在两份相同的属性和方法,很不优雅。

4、寄生式组合继承【终极方案】

为了解决 组合式继承 构造函数被执行两次的问题,我们将 指向父类实例改为指向 拷贝的父类原型,去掉一次构造函数的执行,并且 不会相互影响。

主要是把原来的 ,其他都一样。
Child.prototype = new Parent() => Child.prototype = Parent.prototype
 

但是 问题又出来了,子类原型和父类原型都指向同一个对象,那还是会相互影响
所以:给 父类原型做一个 浅拷贝
Child.prototype = Object.create(Parent.prototype)
到这里 ES5 的所有继承都有了,babel 对 ES6 继承的转换也是 使用了 寄生组合式继承

function Parent(name) {
  this.name = [name];
}
Parent.prototype.getName = function () {
  return this.name;
};
function Child() {
  // 构造函数继承
  Parent.call(this, "zhangsan");
}
//原型链继承
// Child.prototype = new Parent()
Child.prototype = Object.create(Parent.prototype); //将`指向父类实例`改为`指向父类原型`
Child.prototype.constructor = Child;

//测试
const child = new Child();
const parent = new Parent();
child.getName(); // ['zhangsan']
parent.getName(); // 报错, 找不到getName()
 

我们回顾一下实现过程:

  1. 一开始最容易想到的是原型链继承,通过把子类实例的原型指向父类实例来继承父类的属性和方法,但原型链继承的缺陷在于对子类实例继承的引用类型的修改会影响到所有的实例对象以及无法向父类的构造方法传参。
  2. 因此我们引入了构造函数继承, 通过在子类构造函数中调用父类构造函数并传入子类 this 来获取父类的属性和方法,但构造函数继承也存在缺陷,构造函数继承不能继承到父类原型链上的属性和方法。
  3. 所以我们综合了两种继承的优点,提出了组合式继承,但组合式继承也引入了新的问题,它每次创建子类实例都执行了两次父类构造方法,我
  4. 们通过将子类原型指向父类实例改为子类原型指向父类原型的浅拷贝来解决这一问题,也就是最终实现 —— 寄生组合式继承

回复

我来回复
  • 暂无回复内容