# defineProperty 方法,JS 公有、私有、静态,栈和队列

TIP

本章节我们会讲解以下三方面内容

  • 如何利用 Object.defineProperty()方法给一个对象新增属性
  • 公有,私有,静态属性和方法的写法和应用,在此基础上了解什么是特权方法
  • 学习栈和队列这两种数据结构,同时利用 JS 来实现一个简单的栈和队列

# 一、Object.defineProperty

TIP

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

// 语法
Object.defineProperty(obj, prop, descriptor);
  • obj :要定义属性的对象
  • prop :要定义或修改的属性的名称
  • descriptor : 要定义或修改的属性描述符,是一个对象

目前存在的属性描述符有两种主要形式:数据描述符存取描述符

# 1、数据描述符

TIP

  • 数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。
  • 数据描述符有以下 4 个特性描述它们的行为,具体如下表
属性特性 说明 默认值
writable 表示属性值是否可以被修改,false 不能改,true 可以修改 false
value 属性的实际值 undefined
configurable 特性表示对象的属性是否可以被删除,以及除 valuewritable 特性外的其他特性是否可以被修改。 false
enumerable 表示属性是否可通过 for-in 循环遍历。 false

Object.defineProperty定义的属性,如果属性描述符为数据描述符,那这个属性被称为 “数据属性”

# 1.1、writable 与 value

TIP

  • writable 表示属性值是否可以被修改,false 不能改,true 可以修改,默认值 false
  • value 表示属性的实际值,默认值为 undefined
// 定义对象
var obj = {
  name: "张三",
};
// 定义属性age
Object.defineProperty(obj, "age", {
  value: 22,
  writable: false,
});
console.log(obj);
// age的值并不能被修改成功
obj.age = 23;
console.log(obj);

image-20221122235939469

// 定义对象
var obj = {
  name: "张三",
};
// 定义属性age
Object.defineProperty(obj, "age", {
  value: 22,
  writable: true,
});
console.log(obj);
// age的属性值被成功修改
obj.age = 23;
console.log(obj);

image-20221123000043901

# 1.2、enumerable

TIP

  • enumerable 表示属性是否可通过for-in等其它方式循环遍历,false表示不可以,true表示可以
  • 默认值 false
// 定义对象
var obj = {
  name: "张三",
};
// 定义属性age
Object.defineProperty(obj, "age", {
  value: 22,
  writable: true,
  enumerable: false, // 不可以被枚举,被for..in遍历
});
console.log(obj);
// age属性不能被遍历出来
for (var key in obj) {
  console.log(obj[key]);
}

image-20221123000500106

// 定义对象
var obj = {
  name: "张三",
};
// 定义属性age
Object.defineProperty(obj, "age", {
  value: 22,
  writable: true,
  enumerable: true, // 可遍历
});
console.log(obj);
// age属性不能被遍历出来
for (var key in obj) {
  console.log(obj[key]);
}

image-20221123000623949

# 1.3、configurable

TIP

configurable 为 true,表示可删除属性重新定义,其它特性也可以被修改

var obj = {};
// 定义属性
Object.defineProperty(obj, "name", {
  writable: true,
  value: "张三",
  enumerable: true,
  configurable: true,
});
console.log(obj);
delete obj.name; // 删除属性
Object.defineProperty(obj, "name", {
  writable: false,
  value: "张三",
  enumerable: false,
  configurable: false,
});
console.log(obj);

configurable 为 false 表示不可删除属性重新定义,除 valuewritable 特性外的其他特性不可以被修改。

var obj = {};
// 定义属性
Object.defineProperty(obj, "name", {
  writable: true,
  value: "张三",
  enumerable: true,
  configurable: false,
});

// 不可删除
// delete obj.name;

Object.defineProperty(obj, "name", {
  writable: false, // 可修改
  value: "张三2", // 可修改
  enumerable: true, // 不可修改
  configurable: true, // 不可修改
});
console.log(obj);

# 1.4、注意事项

注:

  • 直接定义在对象身上的属性,以上属性特性的默认值分别为:writable:truevalue:undefinedconfigurable:trueenumerable:true
  • 通过Object.defineProperty方式定义的属性,其属性特性的默认值分别为:writable:falsevalue:undefinedconfigurable:falseenumerable:false

# 1.5、Object.getOwnPropertyDescriptor

Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符

Object.getOwnPropertyDescriptor(obj, prop);
var obj = {
  name: "张三",
  age: 23,
};

Object.defineProperty(obj, "sex", {
  writable: false,
  value: "女",
  enumerable: false,
  configurable: false,
});

var descriptor1 = Object.getOwnPropertyDescriptor(obj, "name");
var descriptor2 = Object.getOwnPropertyDescriptor(obj, "sex");
console.log(descriptor1);
console.log(descriptor2);
console.log(obj);

image-20221206201401381

# 2、存取描述符

存取描述符有以下 4 个特性描述它们的行为,具体如下表

属性特性 说明 默认
configurable 特性表示对象的属性是否可以被删除,以及除 valuewritable 特性外的其他特性是否可以被修改。 false
enumerable 表示属性是否可以通过 for...in 循环遍历 false
get 获取函数 获取函数,在读取属性时调用。这个函数的主要责任就是返回一个有效的值 undefined
set 设置函数 设置函数,在写入属性时调用。这个函数决定了对数据做什么样的修改,这个函数有一个参数。 undefined

注:

Object.defineProperty定义的属性,如果属性描述符为存取描述符,那这个属性被称为 “访问器属性”

  • configurable 与 enumerable 特性与 数据描述符的用法是一样的
  • get 获取函数,在读取属性时调用,这个函数的返回值为这个属性的值
  • set 设置函数,在写入属性时调用,这个函数决定了对数据做什么样的修改
var obj = {
  name: "张三",
  age: 23,
  _sex: "女",
  identity: "女士",
};

Object.defineProperty(obj, "sex", {
  set: function (value) {
    this._sex = value;
    if (value === "女") {
      this.identity = "女士";
    } else {
      this.identity = "先生";
    }
  },
  get: function () {
    return this._sex;
  },
});
obj.sex = "男";
console.log(obj.sex);
console.log(obj.identity);

image-20221123010917757

访问器属性的典型应用场景:

当设置或获取一个属性的值时,我们还需要做相关的其它操作,就可以把这个属性设置成访问器属性。

get 和 set 方法的这种机制,我们可以理解为数据拦截或数据劫持。

也就是在我操作数据时,会被 get 和 set 方法拦截,然后在里面做相应的操作。改变正常的访问和设置行为。

# 2.1、注意事项

注:

  • 获取函数和设置函数不一定都要定义
  • 只定义获取函数意味着属性是只读的,尝试修改属性会被忽略,严格模式会抛错
  • 只有一个设置函数的属性是不能读取的,非严格模式下返回值为 undefined,严格模式下(有可能)会抛错。
"use strict"; // 严格模式下
var obj = {
  name: "张三",
  age: 23,
  _sex: "女",
  identity: "女士",
};

Object.defineProperty(obj, "sex", {
  get: function () {
    return this._sex;
  },
});
obj.sex = "男";
console.log(obj);

# 3、Object.defineProperties

TIP

Object.defineProperties()方法允许我们在一个对象上同时定义多个属性。

Object.defineProperties(obj, props);
  • obj 需要定义和修改属性的对象
  • props 用来修改对应属性的描述符对象
var obj = {
  _sex: "女",
  identity: "女士",
};
Object.defineProperties(obj, {
  name: {
    value: "张三",
    writable: true,
  },
  age: {
    value: 23,
    writable: true,
  },
  sex: {
    get: function () {
      return this._sex;
    },
    set: function (value) {
      if (value === "女") {
        this.identity = "女士";
      } else {
        this.identity = "先生";
      }
      this._sex = value;
    },
  },
});

obj.sex = "男";
console.log(obj);

# 4、经典面试题

TIP

JavaScript 中有没有可能让(a === 1 && a === 2 && a === 3)返回true

这是阿里的一个经典面试题,刚开始一看你觉得这是不可能的,因为一个变量怎么可能同时存在三个不同的值呢? 但你静下心来分析,你就能找到面试官在考什么 ?

如果我们在读取一个变量的值时,能修改这个变量对应的值,那不就有解了吗 ?

我们可以把变量 a 当前 window 对象的属性,同时 a 还是一个访问器属性,那我们就可以在他的 get 方法中来修改他的值。

var _a = 0;
Object.defineProperty(window, "a", {
  get: function () {
    return ++_a;
  },
});

if (a === 1 && a === 2 && a === 3) {
  console.log("再次输出a的值为" + a);
}

注:

通过上面这个面试题给了我们一个启发

如果在获取或设置一个变量的值时,还需要做相关的其它操作,我们就可以把这个变量设置成一个访问器属性,然后在他的 get 和 set 方法中做相应的操作。

# 4.1、案例应用:追溯属性的赋值记录

TIP

当我们每次设置属性的值时,可以把设置的值保存在数组中,那这个数组就是用来保存属性赋值的记录

var obj = {
  _num: 0,
  _historyValues: [],
};
Object.defineProperty(obj, "num", {
  get: function () {
    return this._num;
  },
  set: function (value) {
    this._historyValues.push(value);
    this._num = value;
  },
});

obj.getHistory = function () {
  return this._historyValues;
};
obj.go = function (index) {
  if (index >= this._historyValues.length) throw new Error("访问下标超出范围");
  return this._historyValues[index];
};

obj.num = 1;
obj.num = 2;
obj.num = 3;
console.log(obj.getHistory());
console.log(obj.go(1));
console.log(obj.go(3));

# 4.2、应用案例:数据驱动页面更新(单向)

TIP

Vue 中有两种数据绑定方式

  • 单向绑定(v-bind):数据只能从 data 流向页面
  • 双向绑定(v-modle):数据不仅能从 data 流向页面,还能从页面流向 data

在 Vue2 中,其数据的绑定方式底层采用的是 Object.defineProperty,在 Vue3 中,底层采用的是 Proxy 代理。但本质的原理是一样的。

以下案例简单实现了数据的单向绑定,关于双向绑定后面的案例中会讲到

<div class="J-container">
  <h3 class="title"></h3>
  <img src="" alt="" class="main-img" width="200" />
  <p>价格 <span class="price"></span></p>
</div>

<script>
  //   var data = {
  //     title: "xx",
  //     mainImg: "xx",
  //     price: "xx",
  //   };

  function defineData() {
    var _obj = {},
      title = "云原生容器化docker+K8S+CICD弹性扩容集群架构实战",
      mainImg =
        "https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2020/04-23/1339186404a1276893.jpg",
      price = 3680.0;

    // 获取页面元素
    var oTitle = document.querySelector(".title");
    var oMainImg = document.querySelector(".main-img");
    var oPrice = document.querySelector(".price");

    // 初始渲染
    oTitle.innerText = title;
    oMainImg.src = mainImg;
    oPrice.innerText = price;

    var data = Object.defineProperties(_obj, {
      title: {
        get: function () {
          return title;
        },
        set: function (value) {
          title = value;
          oTitle.innerText = value;
        },
      },
      mainImg: {
        get: function () {
          return mainImg;
        },
        set: function (value) {
          mainImg = value;
          oMainImg.src = value;
        },
      },
      price: {
        get: function () {
          return price;
        },
        set: function (value) {
          price = value;
          oPrice.innerText = value;
        },
      },
    });

    return data;
  }

  var data = defineData();

  // 当data中的数据发生更新数据,则页面数据就发生相应的变化
  data.title = "Web前端高级工程师系统课-星辰班";
  data.mainImg =
    "https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/08-29/210311f40bcf290736.jpg";
  data.price = 300;
</script>

# 二、区分公有、静态、私有属性

TIP

深入浅出 JavaScript 公有属性和方法,静态属性和方法,私有属性和方法、特权方法,混合模式,综合应用实践 多彩运动的小球

# 1、JS 公有属性和公有方法

TIP

  • 公有属性:所有构造函数的实例都可以访问的属性,在构造函数内部通过 this.属性名 定义的。
  • 公有方法:所有构造函数的实例都可以访问的方法,在构造函数 prototype 原型上定义的方法。

公有属性也称实例属性,公有方法也称实例方法

function Person(name, age) {
  // 公有属性 实例属性
  this.name = name;
  this.age = age;

  // 公有方法(一般不会这样写)
  this.toSleep = function () {
    console.log("我正在睡觉");
  };
}
// 公有方法 实例方法
Person.prototype.sayHello = function () {
  console.log("大家好,我是" + this.name);
};

var p1 = new Person("张三", 32);
console.log(p1.name);
console.log(p1.age);
p1.sayHello();

# 2、JS 静态属性和静态方法

TIP

  • 静态属性:只有类(构造函数)本身能访问的属性,通过类名.属性名 来定义
  • 静态方法:只有类(构造函数)本身能访问的属性,通过类名.方法名 = function() {...} 来定义
function Person() {}
// 静态属性
Person.length = 0;
Person.children = function () {
  console.log("静态方法");
};

var p = new Person();
console.log(p.length); // 不能访问
console.log(Person.length); // 能访问

# 3、JS 私有属性、私有方法和特权方法

TIP

  • 私有属性:只能在构造函数内部才能访问的属性,如果外部要访问必须通过指定的方法来访问和修改
  • 私有方法:是指对象不希望公开的方法,只能在构造函数内部才能调用的方法
  • 特权方法:是指有权访问内部私有属性和私有方法的公有方法

注意:

  • 在 JS 中并没有私有属性和私有方法的概念,所以需要利用闭包的思想
  • 行业约定规范,私有属性和方法在命名时以 _ 下划线开头
function Price() {
  // 私有属性
  var _price = 0;

  // 私有方法  用来对属性price做相关操作
  function _computed() {
    return _price > 0 ? "¥" + _price : "免费";
  }

  // 特权方法  获取属性计算后的值
  this.getPrice = function () {
    return _computed();
  };

  // 特权方法
  this.setPrice = function (value) {
    if (typeof value !== "number") throw new TypeError("传入一个数字");
    _price = value;
  };
}

var p = new Price();
p.setPrice(200.05);
console.log(p.getPrice()); // ¥200.05
p.setPrice(-90); //
console.log(p.getPrice()); // 免费

# 4、混合模式

TIP

  • 将所有实例都操作的特权方法定义在构造函数的原型链上
  • 特权方法要访问到私有属性和方法可以利用闭包来实现

案例应用

  • 利用 JS 来模拟现实生活中,父亲有挣钱、花钱、查看账户金额的能力,但是他的孩子只有花钱能力

  • 我们可以定义两个类:Father类(模拟父亲) Children类(模拟孩子们)

Father 类身上有的方法和属性

属性与方法 功能
私有属性 _money 记录账户总金额
静态方法 save 存钱
静态方法 take 花(取 take)钱
静态方法 view 查看账户金额
实例方法 take 取钱(提供孩子花钱的接口)

Children 类身上有的方法和属性

属性和方法 功能
实例属性 姓名 保存孩子的姓名
// 父类
var Father = (function () {
  var _money = 0; // 私有属性

  function Father() {}
  // 静态方法 存钱
  Father.save = function (value) {
    console.log("父亲存入" + value + "元");
    _money += value;
  };
  // 静态方法 取钱
  Father.take = function (value) {
    console.log("父亲取出" + value + "元");
    _money -= value;
  };
  // 静态方法 查看
  Father.view = function () {
    console.log("目前账户还有" + _money + "元");
    return _money;
  };

  // 公有方法 (实例方法)
  Father.prototype.take = function (value) {
    _money -= value;
    console.log(this.name + "取了" + value + "元");
  };

  return Father;
})();

// 子类
function Child(name) {
  this.name = "张三";
  Father.call(this); // 经典继承(盗用构造函数)
}
// 原型式继承
Child.prototype = Object.create(Father.prototype);

var child = new Child();
Father.save(2000);
child.take(1000);
Father.view();
Father.save(2000);
Father.view();

image-20221209150311956

注意: 以上情况,所有实例本质上操作的是同一个变量 _money

# 5、多彩运动的小球

GIF2022-11-1615-58-55

首先我们要根据这个特效做相关分析:构建一个什么类,这个类上有那些属性和方法。

# 5.1、多彩运动小球的实现原理

TIP

  • 当鼠标滑动时,会产生一系列的彩色小球,然后这些小球开始向不同的方向运动,运动过程中会发生(大小,位置,透明度)的变化
  • 所以我们需要构建一个球类,这个类身上有以下相关的属性和方法
属性和方法 说明
实例属性 x 小球水平方向坐标 ,默认值 0
实例属性 y 小球垂直方向从标 ,默认值 0
实例属性 r 小球的半径,默认值 20
实例属性 color 数组,从数组中随机取出一个颜色作为小球的颜色
实例属性 opacity 小球的透明度(刚开始透明度为 1)
实例属性 speedX 小球水平方向运动速度(步长)随机 (取值范围[-10,10]
实例属性 speedY 小球垂直方向运动速度(步长)随机(取值范围[-10,10]
实例属性 dom 小球的 dom 结构
实例方法 init 初始化一个小球(根据小球属性,在页面创建一个真实的 DOM 球)
实例方法 update 更新小球的属性值(x,y,r,opacity),同时小球透明度为 0,将其从 DOM 中删除

那如何监控鼠标在滑动过程中被创建出来的一堆小球,然后让他们不停的运动呢 ?

  • 我们需要在球类上创建一个私有属性ballArr = [],用来保存鼠标移动时创建出来的实例化小球。每实例化一个小球,就把这个实例化的小球对象添加到ballArr数组中
  • 还需要创建一个静态方法getBalls用来获取所有实例化的小球。这样我们就能拿到所有实例化的小球,对他们进行操作。
属性和方法 说明
私有属性 _ballArr 数组,用来保存创建好的实例化小球
静态方法 getBalls 用来获取所有实例化的小球 (返回数组 ballArr)

拿到实例化小球后,如何让小球运动起来 ?

  • 要让小球运动起来,需要开启一个定时器,让球不断的调用自身的update方法,实现小球运动及运动中各种属性的变化
  • 同时还要判断如果小球的透明度为 0,则需要将小球从ballArr数组和 DOM 中删除,确保垃圾能及时被回收,不至于小球多了造成页面卡顿

# 5.2、JS 代码实现思路

TIP

创建一个球类,定义好球类的实例属性

/**
 * Ball 创建一个球类
 * @param x坐标  默认值 0
 * @param y坐标  默认值 0
 * @param r小球半径 默认 20
 */
function Ball(x = 0, y = 0, r = 20) {
  this.x = x; // x坐标
  this.y = y; // y坐标
  this.r = r; // 小球半径
  // 随机生成一个小球颜色
  this.color = (function () {
    var color = [
      "red",
      "pink",
      "skyblue",
      "orange",
      "tomato",
      "khaki",
      "greenyellow",
    ];
    var index = (Math.random() * color.length) >> 0;
    return color[index];
  })();
  this.opacity = 1; // 小球透明度
  // 小球运动速度,speedX和speedY的取值范围 [-10,10],但不能同时为0
  do {
    this.speedX = Math.floor(Math.random() * 21) - 10;
    this.speedY = Math.floor(Math.random() * 21) - 10;
  } while (this.speedX === 0 && this.speedY === 0);

  // 在new Ball(),内部会自动调用this.init()初始化小球,在页面显示,其实现代码看下一步
  this.init();
}

// 鼠标在页面滑动时,会创建实例化的小球
document.onmousemove = function (e) {
  var pageX = e.pageX;
  var pageY = e.pageY;
  new Ball(pageX, pageY);
};

实现球类的 init 初始化方法,实现在页面插入一个真实的小球

Ball.prototype.init = function () {
  this.dom = document.createElement("div"); // 创建dom结构
  this.dom.style.position = "absolute";
  this.dom.style.left = this.x - this.r + "px";
  this.dom.style.top = this.y - this.r + "px";
  this.dom.style.width = 2 * this.r + "px";
  this.dom.style.height = 2 * this.r + "px";
  this.dom.style.borderRadius = "50%";
  this.dom.style.backgroundColor = this.color;
  // 添加到页面
  document.body.appendChild(this.dom);
};

实现球类的 update 方法,当调实例调用 update 方法,就可以更新自己的属性

Ball.prototype.update = function () {
  this.x += this.speedX; // 更新x坐标
  this.y += this.speedY; // 更新y坐标
  this.r += 0.3; // 更新半径
  this.opacity -= 0.01;

  // 更新的属性更新到真实DOM上
  this.dom.style.display = "none";
  this.dom.style.width = this.r * 2 + "px";
  this.dom.style.height = this.r * 2 + "px";
  this.dom.style.left = this.x - this.r + "px";
  this.dom.style.top = this.y - this.r + "px";
  this.dom.style.opacity = this.opacity;
  this.dom.style.display = "block";
};

让产生的实例小球能运动起来,需要创建私有属性保存每次创建的实例小球,同时还要创建静态方法,可以获取到创建的所有实例小球

var _ballArr=[]; // 类的私有属性
// 静态方法
Ball.getBalls = function () {
    return ballArr;
};

function Ball(x = 0, y = 0, r = 20{
               // ......
               // .....以下代码放在 this.init() 后面

               // 每次创建的实例对象,添加到数组 _ballArrl中
               _ballArr.push(this);
}

开启定时器,间隔一定时间,让小球调用自己的 update 方法,实现小球运动

var timer = setInterval(function () {
  var balls = Ball.getBalls();
  // 更新小球
  for (var i = 0; i < balls.length; i++) {
    balls[i].update();
  }
}, 20);
  • 如果小球在运动种,透明度变成了 0,则需要从_ballArr 和 DOM 中删除,这样垃圾就能得到及时回收,不会因为小球太多造成页面卡顿。
  • 如果判断小球在运动过程中透明度变为 0,则可以在 update 方法中来判断
// 以下代码添加到update方法的最后面
// 如果小球的透明度小于等于0,则将其从数组和DOM中删除
if (this.opacity <= 0) {
  for (var i = 0; i < _ballArr.length; i++) {
    if (_ballArr[i] === this) {
      _ballArr.splice(i, 1); // 从数组中删除
      document.body.removeChild(this.dom); // 从DOM中删除
      break;
    }
  }
}

# 5.3、完整版源码

TIP

为了防止变量造成全局污染,则利用闭包,将所有代码封装在立即执行函数中,然后将 Ball 类作为返回值返回

<script>
  /**
   * Ball 创建一个球类
   * @param x坐标  默认值 0
   * @param y坐标  默认值 0
   * @param r小球半径 默认 20
   */
  var Ball = (function () {
    var _ballArr = []; // 类的私有属性
    function Ball(x = 0, y = 0, r = 20) {
      this.x = x; // x坐标
      this.y = y; // y坐标
      this.r = r; // 小球半径
      // 随机生成一个小球颜色
      this.color = (function () {
        var color = [
          "red",
          "pink",
          "skyblue",
          "orange",
          "tomato",
          "khaki",
          "greenyellow",
        ];
        var index = (Math.random() * color.length) >> 0;
        return color[index];
      })();
      this.opacity = 1; // 小球透明度
      // 小球运动速度,speedX和speedY的取值范围 [-10,10],但不能同时为0
      do {
        this.speedX = Math.floor(Math.random() * 21) - 10;
        this.speedY = Math.floor(Math.random() * 21) - 10;
      } while (this.speedX === 0 && this.speedY === 0);

      // 每次创建的实例对象,添加到数组 _ballArrl中
      _ballArr.push(this);
      // 每次实例化对象时,就调用init方法,在页面实例化小球
      this.init();
    }

    // 静态方法
    Ball.getBalls = function () {
      return _ballArr;
    };

    Ball.prototype.init = function () {
      this.dom = document.createElement("div"); // 创建dom结构
      this.dom.style.position = "absolute";
      this.dom.style.left = this.x - this.r + "px";
      this.dom.style.top = this.y - this.r + "px";
      this.dom.style.width = 2 * this.r + "px";
      this.dom.style.height = 2 * this.r + "px";
      this.dom.style.borderRadius = "50%";
      this.dom.style.backgroundColor = this.color;
      // 添加到页面
      document.body.appendChild(this.dom);
    };

    Ball.prototype.update = function () {
      this.x += this.speedX; // 更新x坐标
      this.y += this.speedY; // 更新y坐标
      this.r += 0.3; // 更新半径
      this.opacity -= 0.01;

      // 更新的属性更新到真实DOM上
      this.dom.style.display = "none";
      this.dom.style.width = this.r * 2 + "px";
      this.dom.style.height = this.r * 2 + "px";
      this.dom.style.left = this.x - this.r + "px";
      this.dom.style.top = this.y - this.r + "px";
      this.dom.style.opacity = this.opacity;
      this.dom.style.display = "block";

      // 如果小球的透明度小于等于0,则将其从数组和DOM中删除
      if (this.opacity <= 0) {
        // 找到小球实例在数组中的位置,然后将他从数组中删除
        var index = _ballArr.indexOf(this);
        _ballArr.splice(index, 1);
        // 从dom中删除
        document.body.removeChild(this.dom);
      }
    };
    return Ball;
  })();

  // 鼠标在页面滑动时,会创建实例化的小球
  document.onmousemove = function (e) {
    var pageX = e.pageX;
    var pageY = e.pageY;
    new Ball(pageX, pageY);
  };

  // 点击后,创建一个小球让他运动起来
  //   document.onclick = function (e) {
  //     var pageX = e.pageX;
  //     var pageY = e.pageY;
  //     new Ball(pageX, pageY);
  //   };

  var timer = setInterval(function () {
    var balls = Ball.getBalls();
    // 更新小球
    for (var i = 0; i < balls.length; i++) {
      balls[i].update();
    }
  }, 20);
</script>

# 三、JS 实现栈与队列

TIP

接下来我们学习栈和队列这两种数据结构,同时利用 JS 来模拟栈和队列

在之前的课程(算法)章节,我们学习过栈这种数据结构,这里我们先来复习下

# 1、什么是栈

TIP

栈是一种先进后出的数据结构,是一种逻辑结构,一种抽像出来的理论模型

  • 入栈操作( push ):就将新元素放入到栈中,先放的在栈底
  • 出栈操作( pop ):就是将元素从栈中弹出,只有栈顶元素才能出

stack

  • 之前课程中我们简单的用数组来模拟一个栈的出栈和入栈全过程
  • 数组相当于一个栈结构,向数组中 push 添中元素为入栈,从数组尾部 pop 取出元素为出栈
// 声明一个空数组,用来当成栈
var stack = [];

// 向数组中添加元素
for (var i = 0; i < 6; i++) {
  stack.push(i); // 入栈
  console.log(arr);
}

// 取出数组中的元素
for (var i = 0; i < 6; i++) {
  stack.pop(); // 出栈
  console.log(arr);
}

接下来,我们利用 JS 来模拟一个完整的栈对象

# 2、JS 实现栈结构

TIP

构建一个 Stack 类,只要 new Stack() 就能创建一个新的栈

一个基础的栈对象要求有以下方法和属性

方法 说明
push 入栈,向栈中添加元素
pop 出栈,从栈顶部弹出元素
isFull 查看栈是否满
isEmpty 查看栈是否为空
getTop 取出栈顶部元素
clear 清空栈中元素
view 查看当前栈中元素
属性 说明
实例属性: size 查看栈的长度(模拟大小)
私有属性: _stack 数组,模拟栈容器,栈中元素都存在 stack 中
私有属性,不允许直接操作_stack,只能通过给定的接口来操作
/**
 * Stack 栈
 * size 栈的大小(长度)
 */
function Stack(size = 100) {
  this._stack = []; // 私有属性,栈容器
  this.size = size; // 返回栈的大小(长度),可更改
}

// 判断栈是否满,满返回true,否则false
Stack.prototype.isFull = function () {
  return this._stack.length >= this.size ? true : false;
};

// 判断栈是否为空,为空返回true,否则false
Stack.prototype.isEmpty = function () {
  return this._stack.length <= 0 ? true : false;
};

// 入栈
Stack.prototype.push = function (value) {
  if (this.isFull()) {
    throw new Error("栈满,不能再填加元素");
  } else {
    this._stack.push(value);
    return true; // 返回true,表示入栈成功
  }
};

// 出栈
Stack.prototype.pop = function () {
  if (this.isEmpty()) {
    throw new Error("栈空,没有元素可以出栈");
  } else {
    return this._stack.pop(); // 返回出栈元素
  }
};

// 取出栈顶元素
Stack.prototype.getTop = function () {
  return this._stack[this._stack.length - 1];
};

// 查看栈中元素
Stack.prototype.view = function () {
  console.log("当前栈中的元素有");
  this._stack.forEach(function (item) {
    console.log(item);
  });
};

// 清空栈
Stack.prototype.clear = function () {
  this._stack = [];
  return true; // 清空栈成功
};

var stack = new Stack(4);
// 入栈
console.log(stack.push(1)); // true
console.log(stack.push(2)); // true
console.log(stack.push(3)); // true
console.log(stack.push(4)); // true
// 查看栈是否满
console.log(stack.isFull()); // true
// 查看栈元素
console.log(stack.view()); //  [1, 2, 3, 4]
// 出栈
console.log(stack.pop()); // 4
console.log(stack.pop()); // 3
// 查看栈元素
console.log(stack.view()); // [1, 2]
// 查看栈是否满
console.log(stack.isFull()); // false
// 清空栈
console.log(stack.clear()); // true
// 判断栈是否为空
console.log(stack.isEmpty()); // true
// 查看栈元素
console.log(stack.view()); // []

注:

  • 以上栈(数组)的长度是在动态变化的,但最终入栈的个数不能大于栈的size大小
  • 如果栈满,再入栈就会抛出栈满错误提示
  • 如果栈空,再出栈就会抛出栈空错误提示

# 3、JS 实现栈结构 - 优化版

TIP

以上版本,最终用户本质上还是可以通过 stack._stack的方式操作数组

我们可以把私有属性的名字,改成 Symbol 类型,这样用户就真正没有办法访问到该属性了

var Stack = (function () {
  /**
   * Stack 栈
   * size 栈的大小(长度)
   */
  var _stack = Symbol("_stack"); // 生成唯一标识符
  function Stack(size = 100) {
    this[_stack] = []; //  私有属性,栈容器
    var _size = size; // 返回栈的大小(长度),可更改

    Object.defineProperty(this, "size", {
      get: function () {
        return _size;
      },
      // 当对size进行操作时,需要对数组做相关操作
      set: function (value) {
        if (value < _size) {
          this[_stack] = this[_stack].slice(0, value);
          _size = value;
        }
      },
    });
  }

  // 判断栈是否满,满返回true,否则false
  Stack.prototype.isFull = function () {
    return this[_stack].length === this.size ? true : false;
  };

  // 判断栈是否为空,为空返回true,否则false
  Stack.prototype.isEmpty = function () {
    return this[_stack].length === 0 ? true : false;
  };

  // 入栈
  Stack.prototype.push = function (value) {
    if (this.isFull()) {
      throw new Error("栈满,不能再填加元素");
    } else {
      this[_stack].push(value);
      return true; // 返回true,表示入栈成功
    }
  };

  // 出栈
  Stack.prototype.pop = function () {
    if (this.isEmpty()) {
      throw new Error("栈空,没有元素可以出栈");
    } else {
      return this[_stack].pop(); // 返回出栈元素
    }
  };

  // 取出栈顶元素
  Stack.prototype.getTop = function () {
    return this[_stack][this[_stack].length - 1];
  };

  // 查看栈中元素
  Stack.prototype.view = function () {
    console.log("当前栈中的元素有");
    this[_stack].forEach(function (item) {
      console.log(item);
    });
  };

  // 清空栈
  Stack.prototype.clear = function () {
    this[_stack] = [];
    return true; // 清空栈成功
  };
  return Stack;
})();

var stack = new Stack();
// 入栈
console.log(stack.push(1)); // true
console.log(stack.push(2)); // true
console.log(stack.push(3)); // true
console.log(stack.push(4)); // true
// 查看栈是否满
console.log(stack.isFull()); // true
// 查看栈元素
console.log(stack.view()); //  [1, 2, 3, 4]
// 出栈
console.log(stack.pop()); // 4
console.log(stack.pop()); // 3
// 查看栈元素
console.log(stack.view()); // [1, 2]
// 查看栈是否满
console.log(stack.isFull()); // false
// 清空栈
console.log(stack.clear()); // true
// 判断栈是否为空
console.log(stack.isEmpty()); // true
// 查看栈元素
console.log(stack.view()); // []

# 4、什么是队列

TIP

现在我们来学习一种新的数据结构队列

  • 队列是一种线性的数据结构,它的特点是先进先出(First In First Out,简称FIFO),后进后出
  • 队列的出口端叫作队头front),队列的入口端叫作队尾rear
  • 入队enqueue)就是把新元素放入队列中,只允许在队列的队尾放入元素
  • 出队dequeue)就是把元素移出队列,只允许在队列的队头移出元素

image-20221103162913639用数组来模拟队列

image-20221103211210042

var arr = [];
arr.push(1); // 入队
arr.shift(); // 出队

# 5、JS 实现队列

TIP

构建一个 Queue 类,只要new Queue() 就能创建一个新的队列

一个基础的队列对象要求有以下方法和属性

方法 说明
enQueue 入队,向队尾添加元素
deQueue 出队,从队头删除元素
isFull 判断队列是否已满
isEmpty 判断队列是否为空
getFront 取出队头元素
clear 清空队列
view 查看队列中元素
属性 说明
私有属性: _queue 数组,模拟队列容器,队中元素都存在_queue
私有属性,不允许直接操作_queue,只能通过给定的接口来操作
实例属性:size 队列的大小(长度)
<script>
  var Queue = (function () {
    var _queue = Symbol("queue"); // 创建唯一标识符
    function Queue(size) {
      this[_queue] = []; // 私有属性,队列容器
      this.size = size; // 队列的长度(大小)
    }
    // 判断队列是否已满,true表示已满,false表示未满
    Queue.prototype.isFull = function () {
      return this[_queue].length === this.size ? true : false;
    };
    // 判断队列是否为空
    Queue.prototype.isEmpty = function () {
      return this[_queue].length === 0 ? true : false;
    };

    // 入队
    Queue.prototype.enQueue = function (value) {
      if (this.isFull()) {
        throw new Error("队列已满,不能现入队");
      } else {
        this[_queue].push(value);
        return true; //表示入队成功
      }
    };

    // 出队
    Queue.prototype.deQueue = function () {
      if (this.isEmpty()) {
        throw new Error("队列已为空,没有元素可出队");
      } else {
        return this[_queue].shift(); // 返回出队元素
      }
    };

    // 取出队头元素
    Queue.prototype.getFront = function () {
      return this[_queue][0]; // 返回值为undefined表示当前队列已空
    };

    // 查看队列中元素
    Queue.prototype.view = function () {
      this[_queue].forEach(function (item) {
        console.log(item);
      });
    };

    // 清空队列
    Queue.prototype.clear = function () {
      this[_queue] = [];
      return true; // true清空队列成功
    };
    return Queue;
  })();

  var queue = new Queue(3);
  var queue2 = new Queue(3);
  // 判断队列是否为空
  console.log(queue.isEmpty()); // true
  // 入队元素
  queue.enQueue(1);
  queue.enQueue(2);
  queue.enQueue(3);
  //   queue.enQueue(4);
  // 取出队列头部元素
  console.log(queue.getFront()); // 1
  // 查看队列中元素
  console.log(queue.view()); // [1, 2, 3]
  // 查看队列是否已满
  console.log(queue.isFull()); // true
  // 出队
  console.log(queue.deQueue()); // 1
  console.log(queue.deQueue()); // 2
  console.log(queue.deQueue()); // 3
  // 查看队列是否为空
  console.log(queue.isEmpty()); // true
</script>

上述版本缺点

  • 以上版本,在入队时还好,直接尾部插入元素,但是在出队时从队头取出元素,本质上会造成整个数组往后的所有元素都向前移动,非常消耗性能
  • 同时队列的容量大小一直是在不断变化的,而实际上一个队列的大小在刚开始分配时,大小应该是固定才更合理

优化方向

  • 有没有什么办法能实现在出队时,能正常出,不需要动数组中的其它元素呢 ?
  • 同时保证整个出队和入队过程程中,栈的容量大小是固定的

# 6、优化版本 - JS 实现循环队列

TIP

  • 我们可以利用双指针思想,同时采用循环队列的方式来实现
  • 以下图中的队列容量(长度)为 4,需要用长度为 5 的数组来实现。
  • 定义两个指针 frontrearfrontrear分别表示当前队列队头队尾的下标
  • 刚开始初始化的队列为空,则 front = rear = 0
  • 如果队未满,入队一个元素,rear + 1,向右移一位,当rear + 1 === arr.length 时,则 rear = 0
  • 如果队未空,出队一个元素,front + 1,向右移一位,当front + 1 === arr.lenght 时,front = 0

重点强调:队列的容量 + 1 = 数组的长度

image-20221104001903505

队列入队的整个过程

image-20221103235611430

队列出队与队入的整过程

image-20221103235647233

队列出队的整个过程

通过以上绘图分析得出如下结论


  • 队满: 当(rear + 1) % arr.lenght === front 时,表示队满
  • 队空:rear === front 时,表示队空

出队

  • 出队时要判断当前队是否为空,如果为空,啥也不做。
  • 如果队不为空,要判断 front+1 === arr.length 如果成立则出队后,front = 0
  • 如果不成立,则 front + 1

front 的计算公式:front = (front + 1) % arr.length

入队

  • 入队时要判断当前队是否满,如果满,啥也不做
  • 如果队未满,要判断 rear + 1 === arr.lenght 如果成立,则入队后,rear = 0
  • 如果未满,则 rear + 1

rear 的计算公式:rear = (rear + 1) % arr.length

<script>
  var Queue = (function () {
    var _queue = Symbol("queue");
    var _front = Symbol("front");
    var _rear = Symbol("rear");
    function Queue(size = 100) {
      // 私有属性
      this[_queue] = new Array(size + 1); // 队容器(固定长度)
      this[_front] = 0; // 队头
      this[_rear] = 0; // 队尾
      // 实例属性
      var _size = size;
      Object.defineProperty(this, "size", {
        get: function () {
          return _size;
        },
        set: function () {
          throw new Error("不允许设置size的值");
        },
      });
    }

    // 队是否为空
    Queue.prototype.isFull = function () {
      //   队满的条件
      return (this[_rear] + 1) % this[_queue].length === this[_front];
    };
    // 队是否为空
    Queue.prototype.isEmpty = function () {
      return this[_front] === this[_rear];
    };
    // 入队
    Queue.prototype.enQueue = function (value) {
      //入队前要判断当前队列是否已满,如果已满,则啥也不做,抛出错误提示
      if (this.isFull()) throw new Error("当前队列已满,不能再入队");

      // 先在当前位置入队元素,然后指向右移动
      this[_queue][this[_rear]] = value;
      // 判断当前是不是在数组的最后面,最后一位就回到0
      this[_rear] = (this[_rear] + 1) % this[_queue].length;
      return true;
    };
    // 出队
    Queue.prototype.deQueue = function () {
      // 出队前要判断当前队列是否为空
      if (this.isEmpty()) throw new Error("当前队列已空,没有元素可出队");
      // 只存要出队的元素
      var deQueueValue = this[_queue][this[_front]];
      // 出队不是真的把这个元素从数组中删除,而是把指针移动到下一位
      // 为了能看到效果,我们把出队的元素的位置值设置为null
      this[_queue][this[_front]] = null;
      // 判断出对的指针是否指向了数组的最后面,如果是,则出队后指向0
      this[_front] = (this[_front] + 1) % this[_queue].length;

      return deQueueValue;
    };

    // 队头元素
    Queue.prototype.getTop = function () {
      // 返回出队的元素,如果为null表示当前队列为空
      return this[_queue][this[_front]];
    };
    // 查看队列元素
    Queue.prototype.view = function () {
      var result = this[_queue].filter(function (item) {
        return item !== null;
      });
      return result;
    };
    // 清空队列
    Queue.prototype.clear = function () {
      // 清空队列,就是重新初始化队列
      this[_queue] = new Array(this.size + 1);
      this[_front] = this[_rear] = 0;
      return true;
    };
    return Queue;
  })();

  var queue = new Queue(4);
  console.log(queue.view()); // [empty × 5]
  console.log(queue.enQueue(1)); // true
  console.log(queue.enQueue(2)); // true
  console.log(queue.enQueue(3)); // true
  console.log(queue.enQueue(4)); // true
  console.log(queue.view()); // [1, 2, 3, 4, empty]
  //   console.log(queue.enQueue(4));
  console.log(queue.deQueue()); // 1
  console.log(queue.deQueue()); // 2
  console.log(queue.deQueue()); // 3
  console.log(queue.deQueue()); // 4
  console.log(queue.view()); // [null, null, null, null, empty]

  console.log(queue.isEmpty()); // true
  console.log(queue.isFull()); // false
  //   console.log(queue.deQueue());
  //   console.log((queue.size = 7));
  //   console.log(queue.size);
</script>

# 四、重难点总结

TIP

总结本章重难点知识,理清思路,把握重难点。并能轻松回答以下问题,说明自己就真正的掌握了。

用于故而知新,快速复习。

# 1、重点

TIP

  • 掌握Object.defineProperty()的用法
  • 区分公有、私有、静态属性和方法
  • 了解什么是栈,利用 JS 简单实现一个栈和队列

# 2、难点

TIP

  • 利用 Object.defineProperty 实现数据驱动页面更新
  • 手写多彩运动的小球
  • 利用 JS 实现循环队列
上次更新时间: 6/8/2023, 9:23:17 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X