从js看面向对象

首先明确一下这几个概念

  • 函数是一种可调用的对象,对象都是通过函数创建的
  • 每一个函数都有一个原型对象, 每一个原型对象都有一个指向构造函数的指针;
  • 实例包含了一个指向原型对象的内部(不可见的)指针;
  • 某个对象的原型链,就是由该对象开始,通过proto属性连接起来的一串对象

创建对象

  • 字面量形式 — 简单快速,但限制了对象的重用
  • 工厂模式 — 可重复创建对象,但无法识别对象类型,且公共方法 eat 重复了多次
function Cat() {
    var cat = new Object()
    cat.name = 'JacksonZhou'
    cat.eat = function() {
        console.log('吃鱼')
    }
    return cat
}
let lazyCat = Cat()
  • 构造函数 — 可识别对象类型,但是仍然不能共享公共函数(eat)
function Cat() {
    this.name = 'JacksonZhou'
    this.eat = function() {
        console.log('吃鱼')
    }
}
let lazyCat = new Cat()
  • 原型方式 — 能共享公共函数,动态地添加原型对象的方法和属性,并直接反映在对象实例上;但不能通过构造函数传参;属性是引用类型时,被多个实例共享容易产生属性篡改的问题
function Cat() {}
Cat.prototype.name = 'JacksonZhou'
Cat.prototype.eat = function(){
    console.log('吃鱼')
}
let lazyCat = new Cat()
  • 构造函数和原型的混合模式 — 可对函数传参;在构造函数内定义实例属性(包括引用类型),而公共属性和方法则利用原型模式定义;直接通过对象字面量给 Person.prototype 进行赋值的时候会导致 constructor 改变;并且通过对象字面量给 Person.prototype 进行赋值,会无法作用在之前创建的对象实例上
function Cat(name, food) {
    this.name = name
    this.food = food
}
Cat.prototype.eat = function() {
    console.log('this.food')
}
let lazyCat = new Cat('Tom', 'fish')
  • 动态原型方式 — 在混合模式基础上,将所有信息封装在了构造函数中;先判断公共函数是否已存在于实例中
function Cat() {
    this.name = name
    if(typeof this.eat !== 'function') {
        Cat.prototype.eat = function() {
            console.log('fish')
        }
    }
}

参考自—>js 创建对象的方法和优缺点

属性/方法

实例属性方法

绑定在将来通过构造函数创建的实例上,并且需要通过这个实例来访问的属性、方法

静态属性方法

绑定在构造函数上的属性方法,需要通过构造函数访问

// 比如我们想取出这个Person构造函数创建了多少个实例
function Person(name, age) {
  this.name = name;
  this.age = age;
  if (!Person.total) {
    Person.total = 0;
  }
  Person.total++;
}

let p1 = new Person("jacksonzhou", 28);
console.log(Person.total); // 1
let p2 = new Person("swr", 28);
console.log(Person.total); // 2

原型属性方法

构造函数 new 出来的实例,都共享这个构造函数的原型对象上的属性方法,类似共享库。

对象(Object)属性

  • __proto__或者[[prototype]]指针,指向构造函数的原型。
  • constructor:默认指向创建当前对象的构造函数

每一个函数新建的时候都有一个默认的 prototype, prototype 这个对象上面默认有一个指向自己的 constructor,所以要注意,当你的原型 prototype 被改了, 实例的 constructor 就变了

对象常用方法

1.hasOwnProperty(prop):检查给定的属性是否在当前对象实例中(而不是在实例的原型中)。作为参数的属性必须以 string 形式指定

function Person() {
  this.name = "jacksonzhou";
}
let per = new Person();

let key = "name";
if (key in p && per.hasOwnProperty(key)) {
  // name存在且仅存在于p对象中
}

2.isPrototypeOf(object):用于检查传入的对象是否是另一个对象的原型。

function Person() {
  this.name = "jacksonzhou";
}
let per = new Person();

let obj = Person.prototype;
obj.isPrototypeOf(per); // obj是per的原型,返回true

3.propertyIsEnumerable(propertyName):用于检查给定的属性是否能枚举,即是否能够使用 for in 语句

4.toLocaleString():返回对象的字符串表示,与环境的地区对应

5.toString():返回对象的字符串表示

6.valueOf(): 返回对象的字符串、number、Boolean 表示

new 创建实例对象的四个步骤

var func = function () {
  // 加入是function func()呢?步骤会不会改变
  this.name = "jacksonzhou";
};
var newFunc = new func();
  • 创建空对象
var obj = new Object();
  • 将空对象的__proto__指向构造函数的prototype
obj.__proto__ = func.prototype;
  • 使用空对象作为上下文(this 指向),并执行构造函数
// 此时空对象里并没有name属性,执行构造函数,将创建name属性
var result = func.call(obj);
  • 返回新对象
// 判断是值类型还是引用类型,引用类型直接返回相应的值;值类型则返回新对象即可
if(typeof(result) === 'object') {
    func = result
} else {
    func = obj
}

new 升级版实现

function new(Con, ...args) {
  let obj = {}
  Object.setPrototypeOf(obj, Con.prototype)
  let result = Con.apply(obj, args)
  return result instanceof Object ? result : obj
}

三大特性

面向对象三大特性就是封装继承和多态,简单理解,对于猫这种动物,它本身就是一个封装好的类,你只需要供它吃喝(输入),它就能表现猫的行为(输出),同时它继承了动物所具有的习性(吃东西等~),而不同的猫因为所处环境或者习性的不同,可能会有不同的表现和行为,这就是多态。

封装

把客观事物封装成抽象的类,隐藏属性和方法的实现细节,仅对外公开接口。

① 在 ES6 之前,没有 class 这个概念,借由原型对象和构造函数来实现

function Cat(name, food) {
  this.name = name; // 公有属性
  this.food = food;
}
Cat.prototype.say = function () {
  // 公有方法
  console.log(this.name + " likes eating " + this.food);
};
Cat.see = function () {
  console.log("这是静态方法,无需实例化可调用");
};
var cat = new Cat("Lazier", "mouse");
cat.say(); // 实例共享原型属性和方法

② ES6 的 class

class Cat {
  constructor(name, food) {
    this.name = name;
    this.food = food;
  }
  static see() {
    console.log("这是静态方法,无需实例化可调用");
  }
  say() {
    console.log(this.name + " likes eating " + this.food);
  }
}
var cat = new Cat("Lazier", "mouse");
cat.say();

以上 class 的基本实现原理如下 ↓

var Cat = function(){
  function Cat(name, food){
    this.name = name
    this.food = food
  }
  // 执行挂载函数,创建类
  createClass(Cat,[{key:"say",value:function(){
    console.log(this.name+" likes eating " + this.food)
  }}],[{key:"see",value:function(){
    console.log('这是静态方法,无需实例化可调用')}])
}

// 定义对象属性
let defineProperties = function(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i]
    Object.defineProperty(target, descriptor.key, descriptor)
  }
}
// 挂载函数,将静态或动态方法分别挂载到Cat和Cat的prototype上
var createClass = function({
  return function(Constructor,protoProps,staticProps){
    if(protoProps){ // 原型方法
      defineProperties(Constructor.prototype,protoProps)
    }
    if(staticProps){ // 静态方法
      defineProperties(Constructor,staticProps)
    }
  }
})

了解面向对象的公有、私有、静态属性和方法可以看下面这篇文章的总结

js 面向对象之公有、私有、静态属性和方法详解

继承

子类可以使用父类的所有功能,并且对这些功能进行扩展。继承的过程,就是从一般到特殊的过程。

js 实现继承有多种方式

  • 原型链继承
// 将子类的prototype指向父类的实例
function Parent() {}
function Son() {}
Son.prototype = new Parent();
// * 把Son的原型对象的constructor指向Son,解决类型判断问题
Son.prototype.constructor = Son;
  • 借助构造函数继承(使用 call 和 apply 实现继承)
function Parent() {}
function Son() {
  // 将父类函数中的this,强行绑定为子类的this
  // 可传参
  Parent.call(this, arguments);
}
  • 组合继承
// 原型属性方法由原型链实现继承,实例属性方法由借用构造函数实现继承
// 这样,在原型上定义方法实现了函数复用,又保证每个实例都有它自己的属性
function Parent(name) {
  this.name = name;
}
function Son(name, age) {
  // 继承父类的实例属性方法,之后再添加自己的实例属性方法
  Parent.call(this, name);
  this.age = age;
}
Son.prototype = new Parent();
// 重写Son的原型对象
Son.prototype.constructor = Son;

var demo = new Son("jacksonzhou", 23);
  • 寄生式组合继承 — 现在最常用的继承方法
// 获得父类原型属性方法的副本,解决组合继承的属性重复问题
function inheritPrototype(son, parent) {
  var prototype = Object.create(parent.prototype);
  prototype.constructor = son;
  son.prototype = prototype;
}

function Parent(name) {
  this.name = name;
}
function Son(name, age) {
  Parent.call(this, name);
  this.age = age;
}
Son.prototype = inheritPrototype(Son, Parent);

var demo = new Son("jacksonzhou", 23);

多态

同一操作用在不同对象上,可以产生不同的解释和不同的执行结果

var makeSound = function (animal) {
  animal.sound();
};
// 声明狗的构造函数
var Dog = function () {};
Dog.prototype.sound = function () {
  console.log("汪汪汪");
};
// 声明猫的构造函数
var Cat = function () {};
Cat.prototype.sound = function () {
  console.log("喵喵喵");
};
// 分别调用他们的叫法
makeSound(new Dog());
makeSound(new Cat());
// 非多态写法
var makeSound = function (animal) {
  if (animal instanceof Dog) {
    console.log("汪汪汪");
  } else if (animal instanceof Cat) {
    console.log("喵喵喵");
  }
};
var Dog = function () {};
var Cat = function () {};
// 分别调用他们的叫法
makeSound(new Dog());
makeSound(new Cat());
// 很明显,后续有其他动物加入都要去修改makeSound函数,很不优雅!

这里要介绍下方法重载

方法重载是让类以统一的方式处理不同类型数据的一种手段。表现为多个同名函数同时存在,但具有不同的参数个数或类型。调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法, 这也是一种多态性。

其实 js 本身并没有这个概念,但我们可以通过操作参数的类数组arguments,根据该类数组的长度以及其元素的类型来选择不同的实现,来模拟实现函数重载效果

// js的函数参数相当灵活~可理解成一个动态的类数组
// 不加参数,调用时有传入参数也不会报错
function countCat() {
  if (arguments.length == 1) {
    console.log(`这是一只猫,${arguments[0]}`);
  } else if (arguments.length == 2) {
    console.log(`这是两只猫,${arguments[0]}${arguments[1]}`);
  } else {
    console.log("没猫了~");
  }
}
countCat();
countCat("Tom");
countCat("Tom", "Mary");