JavaScript基础(三)

JavaScript基础(三)

发表于 2024-08-10
更新于 2024-08-24
阅读时长:40分钟
阅读量:16
AI 总结
|

本文内容来源于网络、网课以及作者自己的归纳总结,js系列文章共计四篇,知识点比较多也比较杂,文章仅作简单的总结,不作过多深入分析。系列文章将持续更新。如有不妥之处,请在评论区友好指出~

一、设计模式

1.1 分类

常见的设计模式有23种,大致可分为三种类型

  • 创建型模式:主要解决对象创建问题
  • 结构型模式:主要解决对象组合问题
  • 行为型模式:主要解决对象之间的交互问题

下文仅简单分析JavaScript中常见的几种设计模式

1.2 创建型模式

1、单例模式

目的:需要确保全局只有一个对象(为了避免重复新建,避免多个对象存在互相干扰)

设计方式:通过定义一个方法,使用时只允许通过此方法拿到存在内部的同一实例化对象。在js中通过全局对象或闭包实现单例模式很简单

let Singleton = function (name) {
  this.name = name;
};
Singleton.genInstance = function (name) {
  if (this.instance) return this.instance;
  return (this.instance = new Singleton(name));
};

const instanceOne = Singleton.genInstance("lihua");
const instanceTWO = Singleton.genInstance("xiaoming"); // 不会再新建

console.log(instanceOne, instanceTWO); // instanceOne === instanceTWO

案例1:状态管理,即使多次创建实例,也指向同一个store

function store() {
  this.store = {};
  if (store.install) {
    return store.install;
  }
  store.install = this;
}
var s1 = new store();
var s2 = new store();
s1.store.name = "lin";
console.log(s2);

//输出:{ store:{ name:'lin' } }
// 这样以来,只要创建实例,都会指向同一个对象

案例2:vue-router保障全局有且只有一个,否则会错乱

let _Vue
function install(Vue) {
  if(install.installed && _Vue === vue) return
  install.installed = true
   _Vue = Vue
}
vue.use(router) //每次执行都会指向同一个对象

2、工厂模式

目的:当某一个对象需要经常创建的时候。js中的函数可以充当工厂函数,根据传入参数的同创建不同类型的对象

设计方式:写一个方法,只需要调用该方法,就能拿到想要的对象

function fn(type) {
  switch (type) {
    case:'type1':
      return new Type1() //根据类型创建对象
    //...
  }
}

案例1:创建多个不同的弹窗对象

function infoPop(content, color) {
  this.content = content;
  this.color = color;
}
function confirmPop(content, color) {
    // ...
}

function pop(type, content, color) {
  switch (type) {
    case "infoPop":
      return new infoPop(content, color);
    case "confirmPop":
      return new confirmPop(content, color);
  }
}

// 创建对象
const popInstance1 = pop("infoPop", "hello", "red");
const popInstance2 = pop("confirmPop", "world", "yellow");

1.3 结构型模式

1、适配器模式

目的:处理不兼容的接口或方法之间的适配问题(强调将一个接口转换为另外一个接口)

设计方式:新增一个包装类,对新的方法/接口进行包装以适应旧代码的调用

// 定义目标接口
function TargetInterface() {
  this.request = function () {
    // ...
  };
}
// 定义适配者
function Adaptee() {
  this.specificRequest = function () {
    // ...
  };
}
// 创建适配器
function Adapter() {
  const adaptee = new Adaptee();

  this.request = function () {
    adaptee.specificRequest();
    // 执行适配逻辑
  };
}

// 使用适配器
const adapter = new Adapter();
adapter.request();

案例一:例如你有一个自研框架,其中有方法在你依赖的库中已经实现,那么完全可以不用再写一次该方法(以lodash为例)

myMethods.deepClone = function() {
  return _.cloneDeep.call(this, arguments)
}

2、装饰器模式

目的:在不修改原始对象的情况下,为现有的对象拓展额外的功能

设计方式:假设为一个函数添加新功能,创建一个新的函数并以原始函数为参数,最后返回一个新的函数

案例一:为耗时函数添加缓存功能

// 原始函数
function calculate(num) {
  let result = 0;
  for (let i = 0; i < num; i++) {
    result += i;
  }
  return result;
}

// 装饰器函数
function cache(fn) {
  const cache = new Map(); //闭包
  return function (num) {
    if (cache.has(num)) return cache.get(num);
    else {
      const result = fn(num);
      cache.set(num, result);
      return result;
    }
  };
}

const cachedCalculate = cache(calculate);
console.log(cachedCalculate(99999)); // 执行耗时计算
console.log(cachedCalculate(99999)); // 从缓存中获取结果

案例二:当需要去改动他人的代码,往其中增加新功能时。例如给某些点击事件新增操作提示

function decorator(dom, fn) {
  if (typeof dom.onclick === "function") {
    let _old = dom.onclick;
    //重写
    dom.onclick = function () {
      //调用老方法
      _old();
      //执行新的操作
      fn();
    };
  }
}

3、代理模式

目的:类似于es6的proxy,对目标对象的访问能够被拦截下来,并执行一些其他的操作

// 代理的对象
function TargetFN() {
  this.request = function () {
    // ...
  };
}

// 定义代理处理函数
function Proxy() {
  const targetFN = new TargetFN();
  this.request = function () {
    // 执行一些前置操作
    targetFN.request();
    // 执行一些后置操作
  };
}

const p = new Proxy();
p.request();

1.4 行为型模式

1、策略模式

目的:定义一些列算法,将其逐个封装起来,目的在于分离算法的使用与实现,优化代码,减少重复代码

案例一:优化多重if判断语句

let type = 1;

if (type === 1) {
  console.log("people");
} else if (type === 2) {
  console.log("bird");
} else if (type === 3) {
  console.log("fish");
} else if (type === 4) {
  //...
}

// 使用策略模式
const actions = {
  1: () =>  { console.log("people") },
  2:() =>  { console.log("bird") },
  3: () =>  { console.log("fish") },
  // ...
};

actions[type]()

案例二:优化多重if判断语句嵌套

let type = 1; //物种类型
let actionsType = 1; //吃饭1,睡觉2,游泳3....

if (type === 1) {
  if (actionsType === 1) {
    // do something
  } else if (actionsType === 2) {
    // do something
  } else if (actionsType === 3) {
    // do something
  }
} else if (type === 2) {
  if (actionsType === 1) {
    // do something
  } else if (actionsType === 2) {
    // do something
  } else if (actionsType === 3) {
    // do something
  }
} else if (type === 3) {
    // ...
} else if (type === 4) {
  //...
}

// 使用策略模式(该部分最终可以抽离出去)
const actions = [
  {
    type: 1,
    actionsType: 1,
    fn: () => {
      //do something
    },
  },
  {
    type: 1,
    actionsType: 2,
    fn: () => {
      //do something
    },
  },
  {
    type: 1,
    actionsType: 3,
    fn: () => {
      //do something
    },
  },
  {
    type: 2,
    actionsType: 1,
    fn: () => {
      //do something
    },
  },
  // ...
];

let fn = actions.find(item => item.type === type && item.actionsType === actionsType);
fn();

2、 观察者模式

在一些文章中,把观察者称为发布订阅模式。但是二者其实有一些小区别

观察者模式指的是一个对象(Subject)维持一系列依赖于它的对象(Observer),当有关状态发生变更时 Subject 对象则通知一系列 Observer 对象进行更新

1、结构:两个主要角色:观察者(Observer)和被观察者(Subject)

2、联系:观察者直接订阅被观察者,被观察者维护一个观察者列表,并在状态变化时通知所有观察者(通常调用观察者的方法进行通知)

案例一

以某个手机的销售场景为例。假设现有一爆款手机在手机店卖断了货,手机店(Subject被观察者)告诉前来的客户(Observer观察者)说把姓名联系方式留下(触发add操作),后续有货了通知(触发notify)客户来买

// Subject 对象(客户在这里留了姓名联系方式)
function Subject() {
  this.observers = [];
}
Subject.prototype = {
  add(observer) {
    // 添加(添加客户的姓名联系方式)
    this.observers.push(observer);
  },
  notify() {
    // 通知(通知客户有货了)
    var observers = this.observers;
    for (var i = 0; i < observers.length; i++) {
      observers[i].update();
    }
  },
};

// Observer 对象
function Observer(name) {
  this.name = name; // (用户信息、联系方式等)
}
Observer.prototype = {
  update() {
    // 更新(通知客户来取货)
    console.log("my name is " + this.name);
  },
};

var sub = new Subject();
var obs1 = new Observer("客户1");
var obs2 = new Observer("客户2");
sub.add(obs1);
sub.add(obs2);
sub.notify();

1.5 发布订阅模式

发布订阅模式:订阅者(Subscriber)把自己想订阅的事件注册到(subscribe)调度中心(中介者)。当发布者(Publisher)发布该事件到调度中心(也就是事件触发时),由调度中心统一调用订阅者注册的处理代码

1、结构:存在一个中介者来订阅和发布者之间的关系

2、联系:发布者和订阅者不直接耦合,两者通过中介者进行通信。发布者将消息发送到中介者,然后中介者将消息传递给所有订阅者

3、通知方式: 订阅者通过向中介者注册感兴趣的事件或主题,中介者在接收到消息后负责将消息分发给所有订阅者

简单案例

class EventEmitter {
  constructor() {
    this.event = [];
  }
  subscribe(callback) {
    this.event.push(callback);
  }
  publish(value) {
    this.event.forEach((callback) => {
      callback(value);
    });
  }
}

const ev = new EventEmitter();

// 订阅
ev.subscribe((args) => {
  console.log(args + ",callback111");
});
ev.subscribe((args) => {
  console.log(args + ",callback222");
});

// 发布
ev.publish("hello");

案例一:事件总线

class EventBus {
  constructor() {
    this.events = {}; // 存储事件及其对应的回调函数列表(中介者或者说调度中心)
  }

  // 订阅事件
  subscribe(eventName, callback) {
    this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,创建一个空的回调函数列表
    this.events[eventName].push(callback); 
  }

  // 发布事件
  publish(eventName, data) {
    if (this.events[eventName]) {
      this.events[eventName].forEach((callback) => {
        callback(data); // 执行回调函数,并传递数据作为参数
      });
    }
  }

  // 取消订阅事件
  unsubscribe(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(
        (cb) => cb !== callback
      ); // 过滤掉要取消的回调函数
    }
  }
}

// 创建全局事件总线对象
const eventBus = new EventBus();

const callback1 = (data) => {
  console.log("Callback 1:", data);
};

const callback2 = (data) => {
  console.log("Callback 2:", data);
};

// 订阅事件
eventBus.subscribe("event1", callback1);
eventBus.subscribe("event1", callback2); 

// 发布事件
eventBus.publish("event1", "Hello, world!");

// 取消订阅事件
eventBus.unsubscribe("event1", callback1);

// 发布事件
eventBus.publish("event1", "ahahhaha!");

案例二:利用发布订阅实现防止接口重复调用

import axios from "axios";

let instance = axios.create({
  baseURL: "/api",
});

// 发布订阅
class EventEmitter {
  constructor() {
    this.event = {};
  }
  on(type, cbres, cbrej) {
    if (!this.event[type]) {
      this.event[type] = [[cbres, cbrej]];
    } else {
      this.event[type].push([cbres, cbrej]);
    }
  }

  emit(type, res, ansType) {
    if (!this.event[type]) return;
    else {
      this.event[type].forEach((cbArr) => {
        if (ansType === "resolve") {
          cbArr[0](res);
        } else {
          cbArr[1](res);
        }
      });
    }
  }
}

// 根据请求生成对应的key
function generateReqKey(config, hash) {
  const { method, url, params, data } = config;
  return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join(
    "&"
  );
}

// 存储已发送但未响应的请求
const pendingRequest = new Set();
// 发布订阅容器
const ev = new EventEmitter();

// 添加请求拦截器
instance.interceptors.request.use(
  async (config) => {
    let hash = location.hash;
    // 生成请求Key
    let reqKey = generateReqKey(config, hash);

    if (pendingRequest.has(reqKey)) {
      // 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
      // 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
      let res = null;
      try {
        // 接口成功响应
        res = await new Promise((resolve, reject) => {
          ev.on(reqKey, resolve, reject);
        });
        return Promise.reject({
          type: "limiteResSuccess",
          val: res,
        });
      } catch (limitFunErr) {
        // 接口报错
        return Promise.reject({
          type: "limiteResError",
          val: limitFunErr,
        });
      }
    } else {
      // 将请求的key保存在config
      config.pendKey = reqKey;
      pendingRequest.add(reqKey);
    }

    return config;
  },
  function (error) {
    return Promise.reject(error);
  }
);

// 添加响应拦截器
instance.interceptors.response.use(
  function (response) {
    // 将拿到的结果发布给其他相同的接口
    handleSuccessResponse_limit(response);
    return response;
  },
  function (error) {
    return handleErrorResponse_limit(error);
  }
);

// 接口响应成功
function handleSuccessResponse_limit(response) {
  const reqKey = response.config.pendKey;
  if (pendingRequest.has(reqKey)) {
    let x = null;
    try {
      x = JSON.parse(JSON.stringify(response));
    } catch (e) {
      x = response;
    }
    pendingRequest.delete(reqKey);
    ev.emit(reqKey, x, "resolve");
    delete ev.reqKey;
  }
}

// 接口走失败响应
function handleErrorResponse_limit(error) {
  if (error.type && error.type === "limiteResSuccess") {
    return Promise.resolve(error.val);
  } else if (error.type && error.type === "limiteResError") {
    return Promise.reject(error.val);
  } else {
    const reqKey = error.config.pendKey;
    if (pendingRequest.has(reqKey)) {
      let x = null;
      try {
        x = JSON.parse(JSON.stringify(error));
      } catch (e) {
        x = error;
      }
      pendingRequest.delete(reqKey);
      ev.emit(reqKey, x, "reject");
      delete ev.reqKey;
    }
  }
  return Promise.reject(error);
}

export default instance;

发布订阅模式与观察者模式的区别?

1、通信方式的差异:观察者模式中,被观察者直接通知观察者;而在发布订阅模式中,发布者通过中介者联系订阅者

2、直接耦合与间接耦合:观察者模式中,观察者与被观察者直接耦合;发布订阅模式中,发布者和订阅者通过中介者间接耦合

二、面向对象

JavaScript其实支持多种编程范式的,包括函数式编程和面向对象编程

面向对象:

1、JavaScript中的对象被设计成一组属性的无序集合,像是一个哈希表,由key和value组成

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

3、如果值是一个函数,那么我们可以称之为是对象的方法

创建对象

//方式一: new关键字
var obj = new Object()

//方式二: 字面量
var obj = {}

操作对象

//赋值
obj.name = "linming"

//取值
obj.name

//删除值
delete obj.name

有时候我们也希望对一个属性进行比较精准的操作控制,比如不允许对象的某个值被删除、被赋值、不允许哪一个值被遍历等等,这个时候,就可以使用Object.defineProperty

2.1 defineProperty

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

可接收三个参数

1、obj:要定义属性的对象

2、prop:要定义或修改的属性的名称或Symbol

3、descriptor:要定义或修改的属性描述符

返回值:被修改的对象obj

var obj = {
    name: "xiaoming",
    age: 19
}

// 为obj增加一个属性
Object.defineProperty(obj, "height", {
    value: 1.88
})
console.log(obj) //输出:{ name: 'linmnig', age: 19 }

注:通过该函数添加的新属性,默认不可被遍历

属性描述符分类

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

1、数据属性描述符

2、存取属性(访问器)描述符

分类 configurabel enumerable value writable get set
数据描述符 可以 可以 可以 可以 不可以 不可以
存取描述符 可以 可以 不可以 不可以 可以 可以

如何区分:当存在get、set时,不能有value和writable(不能共存),反之同理

数据描述符的特性

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

    • A、直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true

    • B、通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false

  • 2、[[Enumerable]]:表示是否可以通过遍历获得该属性

    • A、直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true

    • B、通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false

  • 3、[[Writable]]:表示是否可以修改属性的值

    • A、直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true

    • B、通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false

  • 4、[[value]]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改

    • A、默认情况下这个值是undefined
var obj = {
    name: 'xiaoming'
}

//原先obj并没有age这个属性
Object.defineProperty(obj, 'age', {
    //默认值为undefined
    //age默认不可修改或删除
    //age不可被遍历
})

存取属性描述符

1、[[Configurable]]——与数据属性描述符一致

2、[[Enumerable]]——与数据属性描述符一致

3、[[get]]:获取属性时会执行的函数,默认为undefined

4、[[set]]:设置属性时会执行的函数,默认为undefined

let obj = {
    name: 'xiaoming',
    _address: '广州'
}

Object.defineProperty(obj, "address", {
    enumerable: true, //可遍历
    configurable: true, //可删除
    get: function() {
        return this._address
    },
    set: function(value) {
        this._address = value
    }
})

get、set的应用

1、隐藏一个私有属性,不希望直接被外界使用和赋值

2、希望截获某一个属性访问时,设置值时的过程

Object.defineProperty(obj, "address", {
    enumerable: true, //可遍历
    configurable: true, //可删除
    get: function() {
        foo()
        return this._address
    },
    set: function(value) {
        bar()
        this._address = value
    }
})

function foo() {
    console.log("取值前截获到了");
}

function bar() {
    console.log("设置值前截获到了");
}


console.log(obj.address);
console.log(obj.address = "深圳");

Object.defineProperties()

Object.defineProperties()方法直接在一个对象上定义多个新的属性或修改现有属性,并且返回该对象

例如:同时配置name、age属性

const obj = {
    _age: 18
}

Object.defineProperties(obj, {
    name: {
        configurable: true,
        enumerable: true,
        writableL: true,
        value: "xiaoming"
    },
    age: {
        configurable: false,
        enumerable: false,
        get: function() {
            return this._age
        },
        set: function(value) {
            this._age = value
        }
    }
})

获取某一个属性的属性描述符

getOwnPropertyDescriptor()

console.log(Object.getOwnPropertyDescriptor(obj, "age"));

//输出:
//{
//  get: [Function: get],
//  set: [Function: set],
//  enumerable: false,
//  configurable: false
//}

获取对象的所有属性描述符

Object.getOwnPropertyDescriptors()

console.log(Object.getOwnPropertyDescriptors(obj));

2.2 批量创建对象

1、工厂模式

工厂模式是一种常见的设计模式

通常我们会有一个工厂方法,通过该工厂方法我们可以产生想要的对象

//基本结构
function createPerson () {
    var p = {}
    return p
}

//创建对象实例
var p1 = createPerson()

具体案例如下

function createPerson (name, age, height, address) {
    var p = {}
    p.name = name
    p.age = age
    p.height = height
    p.address = address
    p.eating = function() {
        this.name + "在吃东西"
    }
    p.running = function() {
        this.name + "在跑步"
    }
    return p
}

var p1 = createPerson('xiaoming', 22, 1.81, "广州")

console.log(p1.eating);
console.log(p1.name);

工厂模式的缺点:获取不到对象的具体类型,我们在打印对象时,对象的类型都是Obejct类型

2、构造函数

构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数

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

function foo() {
    console.log("foo~")
}

//普通方式调用
foo() //输出:foo~

//new调用
new foo()  //输出:foo~

问题:new调用有什么特殊的地方吗?

如果一个函数被使用new操作符调用了,那么它会执行如下操作:

  • 1、在内存中创建一个新的对象(空对象)

  • 2、这个对象内部的[[prototype]]属性(隐式原型)会被赋值为该构造函数的prototype属性

  • 3、构造函数内部的this,会指向创建出来的新对象 {} = this

  • 4、执行函数的内部代码(函数体代码)

  • 5、如果构造函数没有返回非空对象,则返回创建出来的新对象

function Person() {}

var p1 =  new Person() //创建一个{},this = {} ,执行函数体代码,返回新对象

console.log(p1) //输出:Person {}

完整的案例代码

function Person (name, age, height, address) {
    //1.new 调用时创建一个{}
    //2.这里的this = 那个空对象
    //执行函数体,往对象里边添加属性、方法
    this.name = name  
    this.age = age
    this.height = height
    this.address = address

    this.eating = function() {
        console.log(this.name + "在吃东西~")
    }
}

var p1 = new Person("lihua", "18", "2.00", "深圳市")
var p2 = new Person('xiaoming', 22, "1.92", "广州市")
console.log(p1);

p1.eating()
p2.eating()

console.log(p1.eating === p2.eating) //false

构造函数的缺点:从上边的案例中,console.log(p1.eating === p2.eating) //false 我们可以得出,创建出来的实例的函数地址是不同的(即内部创建了不同的内存空间来保存这个函数),但是完全没有必要。但这样子的创建实例多了,就浪费了很多的内存空间

2.3 对象的原型

隐式原型

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

let obj = { name: 'linming' }  //[[prototype]]

如何查看这个属性?

1、方式一:__proto__

通过对象的这个属性可以获取到

let obj = {}
console.log(obj.__proto__)
//输出:[Object: null prototype] {}

2、方式二:Object.getPrototypeOf()

let obj = {}
console.log(Object.getPrototypeOf(obj))
//输出:[Object: null prototype] {}

从上边的代码中,都获取到了一个空对象,这个对象有什么用呢?

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

let obj = {}
obj.__proto__.name = "linming"

console.log(obj.name) //linming

2.4 函数的原型

1、函数本质上也是一个对象,所以它有隐式原型

function foo() {}
console.log(foo.__proto__) // {}

2、函数还有一个显示原型属性:prototype

console.log(foo.prototype)  // {}

思考:函数的prototype也是一个对象,那么这个对象的隐式原型是谁呢?

console.log(foo.prototype.__proto__)
//输出:[Object: null prototype] {},即Object的原型对象

3、当函数作为构造函数被调用时,其内部创造出来的对象的隐式原型会指向显示原型

我们之前讨论过通过new调用一个函数会发生什么?

其中的第二步:这个对象内部的[[prototype]]属性(即隐式原型)会被赋值为该构造函数的prototype属性

function Foo() {
    //1.创建出 {}
    //2. {}.__proto__ = Foo.prototype
    //3.this = {}
    //4.执行代码
    //5.return this 
}

所以,构造函数创建出来的实例,它的隐式原型 === 它的显示原型

let f1 = new Foo()
let f2 = new Foo()

console.log(f1.__proto__ === Foo.prototype)
console.log(f2.__proto__ === Foo.prototype)

所以,这样一来,往Foo.prototype中添加属性,在f1,f2都能访问

Foo.protype.name = 'linming'

console.log(f1.name) //linming
console.log(f2.name) //linming

//通过f1,或者f2向原型对象添加属性
f1.__proto__.age = 22
f2.__proto__.age = 18

console.log(f1.age) //输出:18

4、constructor属性

function Foo() {}

console.log(Foo.prototype) //输出:{}

从上边可以知道,Foo的显示原型对象是空的。

实际上,它不是空的,只是这个原型对象的enumerable属性被设置为false,所以我们不能遍历出其中的属性

原型对象上有一个constructor属性,指向Foo函数本身

console.log(Foo.prototype.constructor)
//输出:[Function: Foo]

所以,下面这样写也是允许的

//获取函数名
console.log(Foo.prototype.constructor.name)  //输出:Foo

//也可以这样
console.log(Foo.prototype.constructor.prototype.constructor.prototype.constructor)
//输出:[Function: Foo]

5、修改原型对象

有时候我们不想用原来的原型对象,可以这么做

function Foo() {}

Foo.prototype = {}  //直接修改了整个prototype

这样的结果是,我们直接断开了指向原本的原型对象,转而指向这个新的{}

Foo.prototype = {
    name: 'linming',
    age: 22,
    height:1.78
}

当然这个新创建的{}对象并不完美,因为 其中没有constructor属性

真实开发中,我们可以通过Object.defineProperty方法添加constructor

Object.defineProperty(Foo.prototype, "constructor", {
    enumerable: false,  //原来的原型对象的constructor是不能被遍历的
    writable: true,
    configurable: true,
    value: Foo //指向本身
})

测试代码

let f1 = new Foo()
let f2 = new Foo()

console.log(f1.name, f2.age, f2.height);

console.log(Foo.prototype.constructor.name); //Foo

2.5 原型链

我们知道,查找一个对象的属性和方法时,会触发[[ get ]]操作

1、在当前对象中查找

2、如果没有找到,将会去对象的原型上(__proto__)查找

const obj = {}
obj.__proto__.address = "广州市"
console.log(obj.address) //输出:广州市

3、如果还没找到,会继续沿着原型链查找(原型对象是一个对象,对象有自己的原型对象)

obj.__proto__ = {}
obj.__proto__.__proto__ = {}
obj.__proto__.__proto__.__proto__ = {
    address: 'linming'
}
console.log(obj.address) //输出:linming

但是原型链不是无限的,最终会止于最顶层原型

那么最顶层的原型是是谁呢?就是Object的原型对象

2.6 Object的原型

Object本质上是一个构造函数,Object也是js所有类的父类

const obj = {}
console.log(obj.__proto__)
//输出:[Object: null prototype] {}

这里输出的obj的隐式原型就是Object的显式原型对象[Object: null prototype] {}

为什么呢?为什么obj的隐式原型可以打印出来的就是顶层原型?

重点理解

const obj = {}
//等于
const obj = new Obejct()

事实上字面量定义一个对象,就是new Object()的语法糖

结合前面说过的调用new创建出一个对象所经过的步骤,就不难理解

const obj = new Object()
//1.创建出一个{}
//2.this = {}
//3.{}.__proto__ = Object.prototype
//4.执行函数体
//5.返回这个对象

所以,这样一来,obj.__proto__ === Object.prototype

那么Object的原型对象里边都有什么呢?

有很多默认的属性和方法

console.log(Object.getOwnPropertyDescriptors(Object.prototype))
//输出:
//constructor
//defineGetter__
//defineSetter__
// __lookupGetter__
//__lookupSetter__
//isPrototypeOf
//toString
//valueOf
//__proto__
//...

注意:其中,顶层显式原型对象是一个对象,它的隐式原型是null

const obj = new Object()
console.log(obj.__proto__.__proto__) //输出:null

构造函数的顶层原型对象

思考:构造函数的显式原型也是一个对象,这个对象是否有[[prototype]]隐式原型属性呢?

答案是肯定的

function People() {}

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

并且该People构造函数的原型对象的原型指向最顶层的Object的原型

所以,可以说People构造函数继承自Object

2.7 对象-函数-原型的联系

1、对象存在隐式原型

const obj = {}
//相当于
const obj = new Object() //new操作,使obj的隐式原型指向Object的显式原型

console.log(obj.__proto__) //顶层Object的显示原型:[Object: null prototype] {}

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

2、函数存在隐式原型、也存在显式原型

隐式原型(对象):Foo.__proto__

显式原型(对象):Foo.prototype

//为什么FOO函数也是一个对象?
let Foo = new Function()

Funtion是Foo的父类(同时,Function本身也是一个对象、也是一个构造函数)

函数的显式原型与隐式原型是不相等的

console.log(Foo.__proto__ === Foo.prototype) //false
//Foo.protorype来自哪里
//答案:创建一个函数时,js引擎自动为其添加的
Foo.prototype = { constructor: Foo }
//Foo.__proto__来自哪里
//答案:new Function(),即Function对象的显式原型
// Foo.__proto__ === Function.prototype

3、关系图

特别注意:

A、Function.__proto__ === Function.prototype(自己创造了自己)

B、Object函数对象也可以理解成const Object = new Function(),所以它的隐式原型指向Function的显式原型

创建一些实例,其指向关系如何

总结

1、对象
在js中,每个对象都存在一个隐式原型,默认是一个空对象,可以通过.__proto__访问
当我们访问js对象中的属性时,如果对象自身没有,则会沿着隐式原型查找,找到则返回,直到顶层对象的显示原型

2、函数
函数因为本质也是对象,所以也存在隐式原型
同时函数也存在显示原型,通过prototype访问,显示原型里有一个constructor属性指向函数本身,同时因为显示原型也是一个对象,它也包含了隐式原型,这个隐式原型指向了顶层对象的原型

3、顶层Object
顶层Object是一个构造函数,它的显示原型中存在很多的方法,例如toString等
它的隐式原型是null

4、构造函数
当我们创建一个实例对象时,默认这个实例对象的隐式原型会指向构造函数的显示原型。所以我们可以通过原型链一层层地查找想要的属性,直到顶层Obecjt

2.8 原型内容补充

1、hasOwnProperty

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

var obj = {
  name: "linming",
  age: 18,
};

obj.__proto__.address = "深圳";

console.log(obj.hasOwnProperty("address")); //false
console.log(obj.hasOwnProperty("name")); //true

2、in操作符

判断某个属性是否在某个对象或者对象的原型上

console.log("address" in obj) //true
console.log("name" in obj) //true

3、for...in

该遍历操作可以遍历出原型上的属性

for (let key in obj) {
    console.log(key)
}
// address name age

4、instanceof

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

假设Student构造函数继承自People类

var stu = new Student()

console.log(stu instanceof Student) //true
console.log(stu instanceof People)  //true
console.log(stu instanceof Object)  //true

2.9 继承

1、原型链继承

// 父类
function People() {
  this.name = "linming";
  this.obj = { age: 20 }
}
People.prototype.running = function () {
  console.log(this.name + "在跑步");
};

// 子类
function Child() {
  this.age = 22;
}

// 继承:改写了Child的显示原型,指向People的实例
Child.prototype = new People();

Child.prototype.eating = function () {
  console.log(this.name + "在吃东西");
};

const son = new Child();
console.log(son.age); //22
console.log(son.name); //linming
son.running(); //linming在跑步
son.eating(); //linming在吃东西

最主要的代码是Child.prototype = new People(),这里将Child的原型改写为People的实例,由此实现了继承

缺点:父类的引用属性会被所有子类共享,一处修改会影响所有子类;子类实例不能传递参数给父类构造函数

2、构造函数继承

function People(name, age, address) {
  this.name = name;
  this.age = age;
  this.address = address;
}

function Child(name, age, address, sno) {
  // this = 所创建的实例
  People.call(this, name, age, address); //相当于在这里执行了People所有初始化方法
  this.sno = sno;
}

const son1 = new Child("linming", 22, "深圳", 20180201);
const son2 = new Child("小明", 223, "广州", 201803041);
console.log(son1);
console.log(son2);

缺点:子类访问不了父类原型上定义的方法

3、组合式继承

组合式继承结合了原型链继承和构造函数继承的特点

function People(name, age, address) {
  this.name = name;
  this.age = age;
  this.address = address;
}

People.prototype.running = function () {
  console.log(this.name + " is running");
};

function Child(name, age, address, sno) {
  // this = 所创建的实例
  People.call(this, name, age, address); //相当于在这里执行了People所有初始化方法
  this.sno = sno;
}

Child.prototype = new People(); //Child的隐式原型即可指向People的显示原型

const son1 = new Child("xiaoming", 22, "深圳", 20180201);
console.log(son1);
son1.running();

优点:父类方法可以复用;允许子类向父类传参;父类中的引用属性不会被共享

4、原型式继承

let person = {
  name: "xiaoming",
  age: 22,
  friends: ["lihua", "tom", "make"],
  sayName: function () {
    console.log(this.name);
  },
};

function objectCopy(obj) {
  function Fun() {}
  Fun.prototype = obj; //让子类的原型指向person
  return new Fun();
}

let p1 = objectCopy(person);
let p2 = objectCopy(person);

p1.friends.push("xxx");
console.log(p2.friends); //多了xxx

缺点:父类的引用会被所有子类共享;子类实例不能向父类传参

5、寄生式继承

let person = {
  name: "xiaoming",
  friends: ["lihua", "make", "jack"],
};

// 创建一个对象,并且允许自定义隐式原型的指向
function copyObject(obj) {
  function Fun() {}
  Fun.prototype = obj;
  return new Fun();
}

function createChild(original) {
  let clone = copyObject(original);
  clone.running = function () {
    console.log(this.name + " is running");
  };
  return clone;
}

const son1 = createChild(person);
const son2 = createChild(person);

son1.friends.push("被共享");
console.log(son2.friends); //['lihua', 'make', 'jack', '被共享']

6、寄生组合式继承

// 父类
function People(name) {
  this.name = name;
  this.friends = ["lihua", "jack", "tom"];
}

// 子类
function Child(name, age) {
  People.call(this, name); //继承父类的属性方法
  this.age = age; // 子类自己的属性方法
}

// 创建一个对象,并且允许自定义隐式原型的指向
function copyObject(obj) {
  function Fun() {}
  Fun.prototype = obj;
  return new Fun();
}

function inheritPrototype(child, parent) {
  let prototype = copyObject(parent.prototype); // 创建对象,对象的隐式原型 = 父类原型
  prototype.constructor = child;
  child.prototype = prototype;
}

People.prototype.sayName = function () {
  console.log(this.name);
};

inheritPrototype(Child, People); //改造子类的原型

Child.prototype.sayAge = function () {
  console.log(this.age);
};

let son1 = new Child("xiaoming", 22);
let son2 = new Child("lily", 20);
son1.friends.push("不会被共享");
console.log(son2.friends); // ["lihua", "jack", "tom"];

son2.sayAge(); // 20
son2.sayName(); // lily

经过这么一番改造后,子类的显示原型被重新赋值了一个对象,并且该对象的隐式原型指向父类的显示原型。因此子类可以访问自己的原型和父类的原型(两者是分隔开的)

另一方面,子类自身拥有父类自身所有的属性和方法(可自定义传参),也可以拥有自己的属性和方法。因此多个子类实例不用担心引用属性被共享的问题

三、class类

3.1 class基础

1、定义类

类本质上就是构造函数的一种语法糖

//普通写法
class People { }

//字面量写法
const People = class { }

2、分析类

因为类是函数,也有隐式原型、显示原型

//隐式原型
console.log(People.__proto__) //输出: {}

//显示原型
console.log(People.prototype) //输出: {}

console.log(People.prototype.constructor) //输出:[class People]

如果检测一个class属于哪个数据类型,最终也会返回function

console.log(typeof People); //输出:function

3、类的构造函数

每个类都有一个构造函数constructor,当使用new操作符时,就会调用这个构造函数

注:不论有没有写这个constructor,创建实例时都会默认执行一次

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

new操作,创建实例

const p1 = new People("xiaoming", 22)
//具体过程:
//1.创建一个{}
//2.{}.__proto__ = People.prototype
//3.this = {}
//4.执行函数体
//5.如果构造函数没有返回非空对象,则返回创建出来的新对象

4、类定义方法

//定义方法就直接在类中定义
class People {
    constructor(name) {
        this.name = name
    }
    eating() {
        console.log(this.name, "正在吃饭")
    }
}

事实上,这样子添加的方法最终会被放在People的显示原型上

console.log(Object.getOwnPropertyDescriptors(People.prototype))
//输出:constructor、eating

const p1 = new People("xiaoming")
const p2 = new People("xiaohong")

p1.eating()
p2.eating()

Object.getOwnPropertyDescriptors()方法用来获取一个对象的所有自身属性的描述符

5、类的访问器方法?

在类中也可以创建访问器方法(get、set),对获取属性、修改属性做一层拦截

class People {
    constructor() {
        this._address = "广州市"
    }
    get address() {
        // 拦截访问操作
        return this._address
    }
    set address(newValue) {
        // 拦截设置操作
        this._address = newValue
    }
}
const p = new People()
console.log(p.address)
p.address = 'xxx'

6、类的静态方法

类的静态方法也称为类方法

普通的方法,是通过实例直接访问,如p1.eating()

类的静态方法,通过类名直接访问,如People.createPerson(),没有必要创建实例

class People {
    static createPerson() {}
}
//访问:People.createPerson()
//案例:随机姓名,随机年龄
class People {
    constructor(name, age) {
        this.name = name
        this.age = age
        this._address = "广州市"
    }
    // 类的静态方法
    static randomPerson() {
        const names = ['吴彦祖','张家辉','彭于晏','胡歌','黎明','刘德华']
        var nameIndex = Math.floor(Math.random() * names.length)
        var name = names[nameIndex]
        var age = Math.floor(Math.random() * 100)
        return new People(name, age)
    }
}
//调用
for(let i = 0; i < 50; i++) {
    console.log(People.randomPerson());
}

静态方法与普通方法的另外一个区别:静态方法存放在类上(所以可以直接调用),普通方法存放在类的显示原型上

3.2 class的继承

7、类的继承

ES6中新增了extends关键字,可以方便我们实现继承

在js中类只能有一个父类:单继承

class Person {
    constructor() {}
}

//继承
class Student extends Person() {}

js引擎在解析子类的时候有要求,如果我们有实现继承,那么子类的构造方法中,在使用this之前,必须调用super

1、调用父类的属性

//Person(父类)
class Person {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
}

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

//创建实例
var stu = new Student("ming", 19, 2323123)
console.log(stu) //输出:Student { name: 'ming', age: 19, sno: 2323123 }

2、调用父类的方法

如果想要在父类方法的基础上进行重写,没有必要整个重写,而是可以从父类继承一些代码逻辑下来

class Person {
    //父类的方法
    personMethod() {
        console.log("逻辑1");
        console.log("逻辑2");
        console.log("逻辑3");
    }
}

class Student extends Person {
    //重写
    personMethod() {
        super.personMethod() //复用原来的逻辑
        console.log("逻辑4");
        console.log("逻辑5");
    }
}

var stu = new Student()
stu.personMethod() //输出:逻辑1、逻辑2、逻辑3、逻辑4、逻辑5

3.3 ES5代码转化

前面提到,class本质上是构造函数的语法糖。最终会被babel转化为浏览器能够认识的低版本代码(一般是ES5代码)

转换方式:https://babeljs.io/

1、基本的转换

//转换前
class Person {}

//转换后
"use strict";

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Person = function Person() {
  _classCallCheck(this, Person);
};

上边的ES5代码,为了防止用户这样子使用类People()——将类作为函数直接调用

这种情况下,会调用_classCallCheck(this, Person),此时this为window/undefined,那么会直接抛出错误

假设去掉这一层判断,class转换后的代码跟一个正常的构造函数没有区别

function Person() { }

2、对属性方法的转换

//转换前
class Person {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    eating() {
        console.log(this.name, "正在吃饭");
    }
}

//转换后
"use strict";

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
      //最终定义了该方法
    Object.defineProperty(target, descriptor.key, descriptor);
  }
}

function _createClass(Constructor, protoProps, staticProps) {
    //普通的方法,定义在原型上
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
    //静态的方法,定义在类上
  if (staticProps) _defineProperties(Constructor, staticProps);
  return Constructor;
}

var Person = /*#__PURE__*/ (function () {
  function Person(name, age) {
    _classCallCheck(this, Person);
    //属性直接添加即可
    this.name = name;
    this.age = age;
  }
    //调用_createClass,添加方法
  (Person, [ 
    {
      key: "eating",
      value: function eating() {
        console.log(this.name, "正在吃饭");
      }
    }
  ]);

  return Person;
})();

以前我们需要在Person.prototype上添加eating方法,这里同样是这样做,只不过换了写法

调用_createClass,判断是普通方法,调用_defineProperties(person原型,需要添加的方法),并使用Object.defineProperty定义了这个方法

3、继承的转换

由于babel转换后,代码量过多,这里不直接展示

结论:class继承的实现,转换为ES5,实际上利用了寄生组合式继承的方法

评论
  • 支持 Markdown 格式
  • 评论需要登录
  • 邮箱会回复提醒(也许会在垃圾箱内)
0 /400 字