2.2-面向对象编程
Create by fall on 08 Sep 2021
Recently revised in 12 Jan 2023
关键字
instanceof
通过该关键字可以判断是否是继承关系
Array instanceof Object // true
let person = function(){}
let no = new person()
no instanceof person//true
instanceOf 原理
function new_instance_of(leftVaule, rightVaule) { 
    let rightProto = rightVaule.prototype; // 取右表达式的 prototype 值
    leftVaule = leftVaule.__proto__; // 取左表达式的__proto__值
    while (true) {
        if (leftVaule === null) {
            return false;    
        }
        if (leftVaule === rightProto) {
            return true;    
        } 
        leftVaule = leftVaule.__proto__ 
    }
}
其实 instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。
new
在 JS 中,函数本身就是构造函数,可以直接通过 new 进行声明构造函数。
new 的作用实际上就是把 function 转换为一个对象
自己生成一个 new
function myNew(fun,...args){
  let obj = {} 
  const res = fun.call(obj,...args)
  obj.__proto__ = fun.prototype
  // 如果存在返回值
  if(res){
    // operate
  }
  return obj
}
function Person(name){
  this.name = name
  this.showName= ()=>{
    console.log(name);
  }
}
Person.prototype.say =function(){
  console.log(this.name+'说:我没钱了');
}
const liu = myNew(Person,'狗剩')
liu.showName()
liu.say()
ES6 为 new 命令引入了一个 new.target 属性,这个属性一般用在构造函数中,返回 new 调用的那个构造函数。
如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,所以这个属性可以用来确定构造函数是怎么调用的,遇到箭头函数会抛错。
function fn(name) {
  console.log('fn:',new.target)
}
fn('nanjiu') // undefined
new fn('nanjiu') 
/*
fn: ƒ fn(name) {
    console.log('fn:',new.target)
}
*/
let fn2 = (name) => {
  console.log('fn2',new.target)
}
fn2('nan') // 报错 Uncaught SyntaxError: new.target expression is not allowed here
Class
ES6 实现的面向对象方式,Class 是 ES6 中创建类的语法糖
语法特性
可以用 class 关键字来定义一个类,类是对一类具有共同特征的事物的抽象,就比如可以把狗定义为一个类,狗有名字会叫也会跳;类是特殊的函数,就像函数定义的时候有函数声明和函数表达式一样,类的定义也有类声明和类表达式,不过类声明不同于函数声明,它是无法提升的;类也有 name 属性
// 类声明
class Dog {
  constructor(name) { // 在创建对象时会构造一次
    this.name = name
  }
  bark() {}
  jump() {}
}
Dog.name  // 'Dog'
// 类表达式:可以命名(类的 name 属性取类名),也可以不命名(类的 name 属性取变量名)
let Animal2 = class {
  // xxx
}
Animal2.name  // 'Animal2'
JS 中的类建立在原型的基础上(通过函数来模拟类,其实类就是构造函数的语法糖),和 ES5 中构造函数类似,但是也有区别,比如类的内部方法是不可被迭代的:
class Dog {
  constructor() {}
  bark() {}
  jump() {}
}
Object.keys(Dog.prototype)  // [] 类的内部方法是不可被迭代的
// 类似于
function Dog2(){}
Dog2.prototype = {
  constructor() {},
  bark() {},
  jump() {},
}
Object.keys(Dog2.prototype)  // ['constructor', 'bark', 'jump'] function可以被迭代
基于原型给类添加新方法
Object.assign(Dog.prototype, {
  eat() {} 
})
- 类声明和类表达式的主体都执行在严格模式下。比如,构造函数,静态方法,原型方法,
getter和setter都在严格模式下执行。 - 类内部的 
this默认指向类实例,所以如果直接调用原型方法或者静态方法会导致this指向运行时的环境,而类内部是严格模式,所以此时的this会是undefined: 
class Dog {
  constructor(name) {
    this.name = name
  }
  bark() {
    console.log( `${this.name} is bark.` )
  }
  static jump() {
    console.log( `${this.name} is jump.` )
  }
}
let dog = new Dog('大黄')
let { bark } = dog
let { jump } = Dog
bark()  // TypeError: Cannot read property 'name' of undefined
jump()  // TypeError: Cannot read property 'name' of undefined
方法和关键字
constructor 方法是类的默认方法,通过 new 关键字生成实例的时候,会自动调用;constructor 默认会返回实例对象:
class Point {}
// 一个类必须有 constructor 方法,如果没有显示定义,则会自动添加一个空的,同下
class Point {
  constructor() {}
}
通过 get 和 set 关键字拦截某个属性的读写操作:
class Dog {
  get age(){
    return 1
  }
}
用 static 关键字给类定义静态方法,静态方法不会存在类的原型上,所以不能通过类实例调用,只能通过类名来调用,静态方法和原型方法可以同名:
class Dog {
  bark() {}
  jump() {
    console.log('原型方法')
  }
  static jump() {
    console.log('静态方法')
  }
}
Object.getOwnPropertyNames(Dog.prototype)  // ['constructor', 'bark', 'jump']
Dog.jump()  // '静态方法'
let dog = new Dog()
dog.jump()  // '原型方法'
new.target 属性允许你检测函数、构造方法或者类是否是通过 new 运算符被调用的。在通过 new 运算符被初始化的函数或构造方法中,new.target 返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target 的值是 undefined,子类继承父类的时候会返回子类:
class Dog {
  constructor() {
    console.log(new.target.name)
  }
}
function fn(){
  if (!new.target) return 'new target is undefined'
  console.log('fn is called by new')
}
let dog = new Dog()  // 'Dog'
fn()                 // 'new target is undefined'
new fn()             // 'fn is called by new'
类的继承
类可以通过 extends 关键字实现继承,如果子类显示的定义了 constructor 则必须在内部调用 super() 方法,内部的 this 指向当前子类:
class Animal {
  constructor(name) {
    this.name = name
  }
  run() {
    console.log(`${this.name} is running.`)
  }
}   
class Dog extends Animal{
  constructor(name){
    super(name)  // 必须调用,就相当于调用了 Animal 的 constructor
    this.name = name
  }
  bark() {
    console.log(`${this.name} is barking.`)
  }
}
let dog = new Dog('大黄')
dog.run()  // '大黄 is running.'
通过 super() 调用父类的构造函数或者通过 super 调用父类的原型方法;另外也可以在子类的静态方法里通过 super 调用父类的静态方法:
// 基于上面的代码改造
class Dog extends Animal{
  constructor(name){
    super(name)  // 调用父类构造函数
    this.name = name
  }
  bark() {
    super.run()  // 调用父类原型方法
    console.log(`${this.name} is barking.`)
  }
}
let dog = new Dog()
dog.bark()s
// '大黄 is running.'
// '大黄 is barking.'
子类的 __proto__ 属性,表示构造函数的继承,总是指向父类;子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的prototype属性:
class Animal {}
class Dog extends Animal {}
Dog.__proto__ === Animal  // true
Dog.prototype.__proto__ === Animal.prototype  // true
子类原型的原型指向父类的原型:
// 基于上面的代码
let animal = new Animal()
let dog = new Dog()
dog.__proto__.__proto__  === animal.__proto__  // true
使用 extends 还可以实现继承原生的构造函数,如下这些构造函数都可以被继承:
String()、Number()、Boolean()、Array()、Object()、Function()、Date()、RegExp()、Error()
class MyString extends String {
  constructor(name){
    super(name)
    this.name = name
  }
  welcome() {
    return `hello ${this.name}`
  }
}
let ms = new MyString('布兰')
ms.welcome()      // 'hello 布兰'
ms.length         // 2
ms.indexOf('兰')  // 1
静态字段
直接通过类来调用,这就称为“静态方法”。
ES 2022 支持静态字段
class MyString  {
  constructor(name){
    this.name = name
  }
  // 通过在类中使用 static 关键字,来声明静态方法
  static welcome() {
    return `hello ${this.name}`
  }
  hello() {
    return `hello ${this.name}`
  }
}
let ms = new MyString('布兰')
ms.welcome()      // 'hello 布兰'
私有字段
ES 2022 支持私有字段
静态公有字段和静态方法一样只能通过类名调用;私有属性和私有方法只能在类的内部调用,外部调用将报错:
class Dog {
	age = 12                   // 公有字段
	static sex = 'male'        // 静态公有字段
	#secret = '我是人类的好朋友'  // 私有字段
	#getSecret() {              // 私有方法
		return this.#secret
	}
}
Dog.sex  // 'male'
let dog = new Dog()
dog.#getSecret()  // SyntaxError
class SampleClass {
    /*
      instead of:
      constructor() { this.publicID = 42; }
    */
    publicID = 42; // public field
    /*
      instead of:
      static get staticPublicField() { return -1 }
    */
    static staticPublicField = -1;
    // static private field
    static #staticPrivateField = 'private';
    //private methods
    #privateMethod() {}
    // static block
    static {
      // executed when the class is created
    }
}
ergonomic brand checks for private fields
Brand checks without exceptions. 📕
class C {
  #brand;
  #method() {}
  get #getter() {}
  static isC(obj) {
    // in keyword to check
    return #brand in obj && #method in obj && #getter in obj;
  }
}
公共和私有字段声明是 JavaScript 标准委员会 TC39 提出的实验性功能(第 3 阶段)。浏览器中的支持是有限的,但是可以通过 Babel 等系统构建后使用此功能。
装饰器
JS 的装饰器还在 Stage 3 draft 阶段,但是 ts 的已经可以用了,虽然可能还会再有改动
装饰器的 Polyfill 使用的是 defineProperty 我个人也不是很清楚 Proxy 和 defineProperty 的使用界限,或许会一方代替另一方。当然这都无关代码开发。
使用的时候要搞清楚一点,进行修饰的始终是个方法,用 @ 后面跟的是个方法,因此也有一部分使用了高阶函数,想了解高阶函数,可以查看
类的装饰器
因为没有对应的装饰器环境,推迟更新,等待下次填坑(2022-01-27)
// 一般用法
// 装饰器一般是一个方法,用来对类进行装饰。该方法内的第一个参数为类
const Knowlage = (specc)=>{
  specc.prototype.name = '411428'
  specc.prototype.speak = function(err){
    console.log(this.locate);
  }
}
@Knowlage
class Student{
  constructor(name){
    this.name = name
  }
  locate="河北省"
  speak(){
    console.log('老子什么都不输出');
  }
}
// 高阶函数用法
const Wisdom = (name, age) => {
  console.log(age);
  return function (specc) {
    specc.prototype.test = '444';
    specc.prototype.speak = function (err) {
      console.log(this.locate);
    }
  }
}
@Wisdom('平淡', 66)
class Talent {
  constructor(name) {
    this.name = name;
  }
  locate = '河北省';
  name;
  speak() {
    console.log('老子什么都不输出');
  }
}
属性的装饰器
装饰器组合
其他的语言已经有装饰器的比较成熟的规范,JS正在进行跟进,哈哈哈哈哈,都拿来吧老子还想学(疯掉.jpg)。
decorator,用于装饰一个类,装饰就在于,你已经有一个非常大的对象了,而你想在其中几个对象中进行修改,但是再创建一个新的对象也不合适,所以就是用装饰器,对其中一部分对象进行修饰(小幅度的改动)。
类的修饰器
function language(value){
  return function(target){
    target.language = value
  }
}
@language('English')
class Country{}
Country.language // English
实例方法修饰器
修饰函数有三个参数
- target 类的 prototype
 - name 修饰的方法名称
 - escriptor 该方法的修饰器
 
// 修饰函数有三个参数
JS的修饰器主要依赖于
Object.defineProperty功能,当解释器遇到了@decorator的语法时,就会调用这个修饰器函数对属性(方法)的描述符(descriptor)进行操作,然后将修饰过的descriptor重新定义属性
ES5 面向对象
class 是 ES6 的语法糖,可以直接声明类,在没有语法糖之前如何实现呢?
对象的封装方式
普通封装(调用方法返回对象)
function creatPhone(name,prise){
  var obj = new Object();
  obj.name = name;
  obj.prise = prise;
  console.log(obj.name);
  console.log(obj.prise);
  return obj;
} 
var xiaomi = creatPhone("小米","996");
var zhongxing = creatPhone("华为","007");
new 封装(通过方法创建对象)
构造函数(使用
new关键字创建的对象)构造函数一般首字母大写
function Phone(name,prise){
  this.name = name;
  this.prise = prise;
  this.showname=function(){
    alert(obj.name);
  };
  alert(this.prise);
} 
var xiaomi = new Phone("小米","996");
xiaomi.showname()
两者区别
- 
普通封装
(方法)没办法继承,只是通过方法创建了个独立对象
执行方法,返回值是创建好的对象
 - 
new 封装
当前函数中的 this 指向新创建的对象
自动返回新创建的对象,可以封装函数可以继承
 
原型对象
原型通常指的是 prototype 和 __proto__ 这两个原型对象。
prototype(显式原型对象):prototype 上的内容会在 new 的时候,作为新对象的属性保留下来。
// 构造函数
function Phone(name, prise) {
  this.name = name;
  this.prise = prise;
};
// 构造函数的原型对象
Phone.prototype.showName = function () {
  alert("选择生产的手机为:" + this.name);
}
Phone.prototype.showWork = function () {
  alert("工作时间为" + this.prise);
}
var xiaomi = new Phone("小米", "996");
var huawei = new Phone("华为", "007");
xiaomi.showName();
huawei.showWork();
__proto__(隐式原型对象):__proto__ 上的内容会作为静态方法在构造函数上直接提供调用
给构造函数上添加原型对象 prototype,能为原型对象添加方法,那么构造函数构造出来的对象共享原型上所有的方法。
prototype 和 __proto__
// function 声明一个类,常用的内容
function Puppy(age) {
  this.puppyAge = age; // this 指向创建后的对象,认为初始是空的即可
}
Puppy.prototype.say = function(){
  console.log('旺旺');
}
Puppy.__proto__.say = function(){
  console.log('阿嘎');
}
// 实例化时可以传年龄参数了
const myPuppy = new Puppy(2);
myPuppy.puppyAge // 证明了指向的是构造后的函数
myPuppy.say() // 调用的是 Puppy 使用 prototype 生成的
Puppy.say() // 调用的是 Puppy 使用 __proto__ 生成的
// console.log(new Puppy()==new Puppy()) // false
Puppy.prototype.say === myDog.say // true
myPuppy.__proto__ === Puppy.prototype // true
Puppy.prototype.__proto__ === Object.prototype // true 说明,所有 new 的构造方法 prototype 都是挂载在一个 new 的 Object 上面,如果要实现继承,只要更改 Puppy.prototype.__proto__ 让它指向父类的 prototype 即可
constructor
一个保留关键字,用来指向 prototype 或者 __proto__ 链接的对象
function Puppy(age) {
  this.puppyAge = age; // this 指向创建后的对象,认为初始是空的即可
}
Puppy.prototype.say = function(){
  console.log('旺旺');
}
Puppy.__proto__.say = function(){
  console.log('阿嘎');
}
const myPuppy = new Puppy(2);
myPuppy.__proto__.constructor.__proto__.say()
myPuppy.say()
如果修改构造函数的 constructor 并不会修改构造函数
或者直接理解,
prototype.constructor只是一个指针,类似于 this 改变 this 只是改变 this 的指向,而不是 this 指向内容的值
function Puppy(age) {
  this.puppyAge = age;
}
// 如果修改 Puppy.prototype.constructor 只会修改 constructor 指针的指向
Puppy.prototype.constructor = function myConstructor(age) {
  this.puppyAge = age + 1;
  console.log(this.puppyAge)
}
const myPuppy = new Puppy(5)
myPuppy2.constructor(999)
静态方法的实现
比较直接的实现
function Puppy(age){
  this.puppyAge = age
}
// name 为保留字,不能设置 Puppy.name
Puppy.cuier = '高'
console.log(Puppy.cuier)
在 Puppy.__proto__ 中修改,会连带修改 Puppy 上的内容
function Puppy(age) {
  this.puppyAge = age
}
Puppy.cuier = '临高'
Puppy.__proto__.cuier = '启明'
console.log(Puppy.cuier)
console.log(Puppy.__proto__.cuier)
console.log(Puppy.__proto__.cuier === Puppy.cuier)
Puppy.__proto__.fake = '虚假'
console.log(Puppy.fake)
console.log(Puppy.__proto__.fake)
console.log(Puppy.__proto__.fake === Puppy.fake)
Puppy.truth = '真实'
console.log(Puppy.truth)
console.log(Puppy.__proto__.truth)
继承的实现
继承:子类继承父类,会让子类拥有所有父类对象上面的内容。
子类上可以添加内容,或者是覆盖父类上面的内容
如何实现继承,要同时实现原型的继承和静态方法的继承。
原型的继承,让 __proto__ 指向父类原型的 prototype 就可以了
function Parent() {}
function Child() {}
Child.prototype.__proto__ = Parent.prototype
但是这样做,只是让 Child 能够访问到 Parent 的  prototype
所以,必须使用
function Parent() {
  this.parentAge = 50
}
function Child() {}
// Child.prototype.__proto__ = Parent.prototype;// 如果通过该方法进行继承,只能让 Child 访问,Child 的子对象无法访问
Child.prototype.__proto__ = new Parent() // 通过让 Child 的原型指向 Parent 来实现继承
const obj = new Child()
console.log(obj.parentAge)
或者使用
function Parent() {
  this.parentAge = 50
}
function Child() {}
Child.prototype = new Parent()
Child.prototype.constructor = Child
// 如果 Child.prototype 指向了 Parent,Child.prototype.constrcutor 也会跟着指向 Parent,而不是 Child
const obj = new Child()
console.log(Parent.prototype.constructor)
静态方法的继承
如果将父对象的原型对象赋值给子对象,会导致两个对象公用同一个原型对象
解决办法,用 for in 循环进行原型对象的赋值
function Dog({name,sex,master}){
  this.name = name;
  this.sex=sex;
  this.master =master;
}
Dog.prototype.file = function(){
  alert(`这只狗是${this.master}先生家的一条狗,是一条${this.sex}${this.name}`);
};
function TinyDog({name,sex,master,weight}){
  Dog.call(this,{
    name:name,
    sex:sex,
    master:master
  });
  this.weight = weight;
};
// 使用for循环进行继承
for(var attr in Dog.prototype){
  TinyDog.prototype[attr] = Dog.prototype[attr];
}
var bomei = new TinyDog({
  name:"博美犬",
  sex:"male",
  master:"老刘",
  weight:0.78
});
bomei.file();
var alasika = new TinyDog({
  name:"阿拉斯加",
  sex:"female",
  master:"老赵",
  weight:1.8
})
// 用构造函数构造的对象,有一个__proto__,指向构造出这个对象的构造函数原型
alert(bomei.__proto__==Dog.prototype);
// instanceof 用于判断某一个对象是否是这个构造函数构造的 
alert(bomei instanceof Dog);
- 继承侧重的是父一级继承的构造函数和方法
 - 多态侧重的是子一级可以重写构造函数和方法
 
其它继承方式
通过 for...in 循环遍历继承
for(var funcName in person.prototype){
  Worker.prototype[funcName] = Person.prototype[funcName];
}
Object.create()
Worker.prototype = Object.create(Person.prototype);
// 使用 Worker 继承 Person
调用函数构造继承
Worker.prototype.__proto__ = new Person();
总结
	
prototype 本身也是对象,所以他也有 __proto__,指向了他父级的 prototype。__proto__ 和 prototype 的这种链式指向构成了 JS 的原型链。原型链的最终指向是Object的原型。Object上面原型链是null,即Object.prototype.__proto__ === null。
prototype.constructor指向的是构造函数,也就是类函数本身。改变这个指针并不能改变构造函数。
为了让实例化出来的对象能够访问到prototype上的属性和方法,实例对象的__proto__指向了类的prototype。所以prototype是函数的属性,不是对象的。对象拥有的是__proto__,是用来查找prototype的。
参考文章
相关文章
| 知识点 | 链接 | 
|---|---|
| 装饰器 | https://www.jianshu.com/p/afef44d449bd | 
| 装饰器 | https://juejin.cn/post/6844903876605280269 | 
| 装饰器 | https://juejin.cn/post/6844904100144889864 | 
| 装饰器 | https://juejin.cn/post/6999451760934780941 |