likes
comments
collection
share

第五章 JavaScript进阶

作者站长头像
站长
· 阅读数 16

第五章 JavaScript进阶

5.1 值类型与引用类型

在 JavaScript 中,值类型和引用类型是两种不同的数据类型,它们有以下几个方面的区别:

  • 1.存储位置:值类型的数据直接存储在变量所在的位置,而引用类型的数据存储在堆内存中,变量中存储的是一个指向堆内存中实际数据的地址。
  • 2.复制方式:值类型的数据在赋值时,会创建一个新的值并将其复制到变量中;而引用类型的数据在赋值时,变量将会引用同一个对象,因此修改其中一个变量会影响到其他变量。 以下是一些例子来说明它们的区别:
// 值类型的例子
var a = 10;
var b = a;
b = 20;
console.log(a); // 10,a 的值没有改变

// 引用类型的例子
var obj1 = { name: 'John' };
var obj2 = obj1;
obj2.name = 'Mike';
console.log(obj1.name); // 'Mike',obj1 和 obj2 引用同一个对象,修改 obj2 会影响到 obj1

在引用类型中,如果我们想要复制一个对象而不是引用同一个对象,可以使用对象的扩展符或 Object.assign() 方法来实现:

var obj1 = { name: 'John' };
var obj2 = { ...obj1 }; // 使用扩展符复制一个对象
var obj3 = Object.assign({}, obj1); // 使用 Object.assign() 方法复制一个对象
obj2.name = 'Mike';
console.log(obj1.name); // 'John',obj1 的值没有改变
console.log(obj2.name); // 'Mike'

需要注意的是,虽然引用类型的数据在复制时会引用同一个对象,但是如果我们重新为变量赋值一个新的对象,这个变量就不再引用原来的对象了:

var obj1 = { name: 'John' };
var obj2 = obj1;
obj2 = { name: 'Mike' }; // 重新赋值,obj2 不再引用原来的对象
console.log(obj1.name); // 'John'
console.log(obj2.name); // 'Mike'

5.2 ==与===的区别

在 JavaScript 中,双等号(==)和三等号(===)都可以用来比较两个值的相等性。但是,它们的行为略有不同:

  • 1.双等号(==)比较两个值的相等性,但是它会进行类型转换。如果两个值类型不同,则会将它们转换为相同的类型,然后再进行比较。这种类型转换可能会导致一些奇怪的行为,因此通常建议尽量使用三等号(===)。
  • 2.三等号(===)比较两个值的相等性,但是它不会进行类型转换。如果两个值类型不同,则它们不相等。只有当两个值类型相同且值相等时,才会返回 true。 下面是一些例子来说明它们的行为差异:
0 == false    // true, 因为在比较前会将 false 转换为 0
0 === false   // false, 因为类型不同
1 == "1"      // true, 因为在比较前会将 "1" 转换为 1
1 === "1"     // false, 因为类型不同
null == undefined  // true, 因为它们都表示“无值”
null === undefined // false, 因为它们是不同的类型

总的来说,如果你想比较两个值的相等性,建议使用三等号(===),这样可以避免一些潜在的问题。

5.3 原型链与继承

JavaScript语言不像面向对象的编程语言中有类的概念,所以也就没有类之间直接的继承,JavaScript中只有对象,使用函数模拟类,基于对象之间的原型链来实现继承关系,ES6的语法中新增了class关键字,但也只是语法糖,内部还是通过函数和原型链来对类和继承进行实现。

原型链

原型链定义

JavaScript对象上都有一个内部指针[[Prototype]],指向它的原型对象,而原型对象的内部指针[[Prototype]]也指向它的原型对象,直到原型对象为null,这样形成的链条就称为原型链。 这样在访问对象的属性时,会现在自己的属性中查找,如果不存在则会到上一层原型对象中查找。 注意:根据 ECMAScript 标准,someObject.[[Prototype]] 符号是用于指派 someObject 的原型。我们可以通过__proto__属性对原型就行访问和设置 从 ECMAScript 6 开始, [[Prototype]] 可以用Object.getPrototypeOf()和Object.setPrototypeOf()对原型进行访问和设置。 例如:

var obj2 = {
  height: 170
}
var obj3 = {
  name: 'obj3'
}
Object.setPrototypeOf(obj3, obj2);
console.log(obj3.height); // 170
var isproto = Object.getPrototypeOf(obj3) === obj2;
console.log(isproto); // true

不同方法创建对象与生成原型链

使用 Object.create 创建对象

ECMAScript 5 中引入了一个新方法:Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数。 例如:

var a = {a: 1};
// a ---> Object.prototype ---> null
 
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)
 
var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null
 
var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因为d没有继承Object.prototype

JavaScript 中所有的对象都是继承自 Object.prototype 的。当创建一个新对象时,它的原型链上会先指向 Object.prototype,然后才是其他的原型链。这也就是为什么所有的对象都有一些共同的方法和属性,例如 toString()、hasOwnProperty() 等。

使用构造函数创建对象

在 JavaScript 中,构造函数其实就是一个普通的函数,一般函数名首字母大写。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数)。例如:

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

Person.prototype = {
  sayName: function () {
      console.log(this.name);
  }
}
var person1 = new Person('zhangsan', 23);
person1.sayName(); // zhangsan

使用构造函数创建对象,经历了如下三个关键步骤:

var temp = {}; //1  创建空对象
Person.call(temp, 'yangyiliang', 23); //2  以空对象为this执行构造函数
Object.setPrototypeOf(temp, Person.prototype); //3  将构造函数的prototype 设置为空对象的原型
return temp;

使用字面量方法创建对象

使用字面量方法创建的对象,根据对象的类型,他们的原型都会指向相应JavaScript内置构造函数的prototype,和直接使用内置构造函数创建对象生成的原型链相同,例如:

var o = {a: 1};
 
// o这个对象继承了Object.prototype上面的所有属性
// 所以可以这样使用 o.hasOwnProperty('a').
// hasOwnProperty 是Object.prototype的自身属性。
// Object.prototype的原型为null。
// 原型链如下:
// o ---> Object.prototype ---> null
 
var a = ["yo", "whadup", "?"];
 
// 数组都继承于Array.prototype
// (indexOf, forEach等方法都是从它继承而来).
// 原型链如下:
// a ---> Array.prototype ---> Object.prototype ---> null
 
function f(){
  return 2;
}
 
// 函数都继承于Function.prototype
// (call, bind等方法都是从它继承而来):
// f ---> Function.prototype ---> Object.prototype ---> null

继承

在面向对象的语言当中,继承关系应该指的是父类和子类之间的关系,子类继承父类的属性和方法,在JavaScript当中是父构造函数和子构造函数之间的关系。 类本身是对象的抽象形式,类的使用价值最后也是在于通过它能够创建对象,所以子类能够继承父类的属性和方法的意义,就是通过子类创建出来的对象能够继承通过父类创建出来的对象的属性和方法。

而这种对象之间的继承关系,就是通过原型链实现。 在1.2.2节中,我们学习到了通过构造函数创建对象的三个重要步骤,其中的一步是把构造函数的prototype对象设置为创建对象的原型。 因此我们将父类的实例对象作为子类的prototype即能够达到继承的目的,如下图所示: 继承的实现

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

Person.prototype.sayName = function () {
  console.log('my name is ' + this.name);
}

function Student (name, age, school) {
  Person.call(this, name, age);
  this.school = school;
}

Student.prototype = Object.create(Person.prototype);

Student.prototype.saySchool = function () {
  console.log('my school is ' + this.school);
}

上面代码实现的继承,遵循了几个原则:

  • 1.因为构造函数创建的对象将公用同一个原型,所以将每个对象独有的属性写在构造函数中,将对象之间可以公用的方法写在构造函数的prototype中,也就是对象的原型中
  • 2.子构造函数继承父构造函数做了两个地方的工作,一是在子构造函数中利用call,调用父构造函数的方法,二是利用Object.create方法创建一个以父构造函数的prototype为原型的对象。利用Object.create而不是直接用new 创建一个实例对象的目的是,减少一次调用父构造函数的执行。
  • 3.先通过prototype属性指向父构造函数的实例,然后再向prototype添加想要放在原型上的方法。最后上一张js高级程序设计第三版中的一张源于原型链继承的图

利用class实现继承 下面利用ES6引入的新语法糖,class、extends关键字对上述实现继承的代码进行改写:

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

  sayName () {
      console.log('my name is ' + this.name);
  }
}

class Student extends Person {
  constructor (name, age, school) {
      super(name, age);
      this.school = school;
  }

  saySchool () {
      console.log('my school is ' + this.school);
  }
}
  • class里的constructor 对应原来的构造函数
  • class里面的其他方法都是写在原来构造函数的prototype中的
  • 子类直接通过extends 关键字进行继承
  • 子类中可以通过super来调用父类中的方法

5.4 This关键字

在 JavaScript 中,this 关键字表示当前执行代码的上下文,它的值根据执行上下文的不同而不同。在使用的时候很容易搞错,下面就进行详细的分析。 1 全局上下文中的this 在浏览器引擎的全局运行上下文中(在任何函数体外部),this 指代全局对象,无论是否在严格模式下。

<script>
    'use strict';
    console.log(this === window); // true
</script>
<script>
    console.log(this === window); // true
</script>

在函数内部,this的值取决于函数是如何调用的。 2 直接调用函数中的this在非严格的模式下,this的值默认为全局对象,window或者global。在严格模式下,this的值为undefined。 一般我们利用this的场景,都不是指代全局对象,所以出现这种this是全局对象或者undefined的时候 往往是我们出错了

<script>
    'use strict';
    function f1 () {
        console.log(this === window);
    }
    f1();// false
</script>
<script>
    function f1 () {
        console.log(this === window);
    }
    f1();// true
</script>

1 调用对象方法中的this当以对象的方法调用函数时,函数中的this指向调用该函数的方法。

var o = {
  prop: 37,
  f: function() {
    return this.prop;
  }
};
console.log(o.f()); // logs 37
/**********************************/
  
var o = {prop: 37};
 
function independent() {
  return this.prop;
}
 
o.f = independent;
 
console.log(o.f()); // logs 37  //只取决于最后的函数调用

函数的this指向调用它的最近对象,这里independent函数中的this指向的是o.b 而不是o。

o.b = {
  g: independent,
  prop: 42
};
console.log(o.b.g()); // logs 42

以下原型链和getter、setter中的this,都是调用对象方法中this的场景。 之前犯过一个反过来的错误,在一个对象中定义的方法中调用this,但是使用方法的时候没有通过对象调用,所以this为undefined而不是原来的对象。 原型链中的 this 相同的概念在定义在原型链中的方法也是一致的。如果该方法存在于一个对象的原型链上,那么this指向的是调用这个方法的对象,表现得好像是这个方法就存在于这个对象上一样。

var o = {
  f : function(){
    return this.a + this.b;
  }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
 
console.log(p.f()); // 5

在这个例子中,对象p没有属于它自己的f属性,它的f属性继承自它的原型。但是这对于最终在o中找到f属性的查找过程来说没有关系;查找过程首先从p.f的引用开始,所以函数中的this指向p。也就是说,因为f是作为p的方法调用的,所以它的this指向了p。这是JavaScript的原型继承中的一个有趣的特性。 getter 与 setter 中的 this 再次,相同的概念也适用时的函数作为一个 getter 或者 一个setter调用。作为getter或setter函数都会绑定 this 到从设置属性或得到属性的那个对象。

function modulus(){
  return Math.sqrt(this.re * this.re + this.im * this.im);
}
 
var o = {
  re: 1,
  im: -1,
  get phase(){
    return Math.atan2(this.im, this.re);
  }
};
 
Object.defineProperty(o, 'modulus', {
  get: modulus, enumerable:true, configurable:true});
 
console.log(o.phase, o.modulus); // logs -0.78 1.414

构造函数中的 this 当一个函数被作为一个构造函数来使用(使用new关键字),它的this与即将被创建的新对象绑定。 注意:当构造器返回的默认值是一个this引用的对象时,可以手动设置返回其他的对象,如果返回值不是一个对象,返回this。

function C(){
  this.a = 37;
}
 
var o = new C();
console.log(o.a); // logs 37
 
function C2(){
  this.a = 37;
  return {a:38};
}
 
o = new C2();
console.log(o.a); // logs 38

在最后的例子中(C2),因为在调用构造函数的过程中,手动的设置了返回对象,与this绑定的默认对象被取消(本质上这使得语句“this.a = 37;”成了“僵尸”代码,实际上并不是真正的“僵尸”,这条语句执行了但是对于外部没有任何影响,因此完全可以忽略它)。 call 和 apply方法 call和apply可以指定函数执行时this的指向。 当一个函数的函数体中使用了this关键字时,通过所有函数都从Function对象的原型中继承的call()方法和apply()方法调用时,它的值可以绑定到一个指定的对象上。

function add(c, d){
  return this.a + this.b + c + d;
}
 
var o = {a:1, b:3};
 
// The first parameter is the object to use as 'this', subsequent parameters are passed as
// arguments in the function call
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
 
// The first parameter is the object to use as 'this', the second is an array whose
// members are used as the arguments in the function call
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

使用 call 和 apply 函数的时候要注意,如果传递的 this 值不是一个对象,JavaScript 将会尝试使用内部 ToObject 操作将其转换为对象。因此,如果传递的值是一个原始值比如 7 或 'foo' ,那么就会使用相关构造函数将它转换为对象,所以原始值 7 通过new Number(7)被转换为对象,而字符串'foo'使用 new String('foo') 转化为对象,例如:

function bar() {
  console.log(Object.prototype.toString.call(this));
}
 
bar.call(7); // [object Number]

bind 方法 ECMAScript 5 引入了 Function.prototype.bind。调用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到了bind的第一个参数,无论这个函数是如何被调用的。 可以看到,call和apply是不改变原来的函数,只是在执行的时候指定函数的this,而bind方法则是生成了一个this指向固定的函数,

function f(){
  return this.a;
}
 
var g = f.bind({a:"azerty"});
console.log(g()); // azerty
 
var o = {a:37, f:f, g:g};
console.log(o.f(), o.g()); // 37, azerty

在ES6的语法中,箭头函数默认绑定当前函数声明环境的this。 DOM事件处理函数中的 this 当函数被用作事件处理函数时,它的this指向函数所绑定的DOM对象。 event.currentTarget指向事件所绑定的元素,而event.target始终指向事件发生时的元素所以this 始终和 event.currentTarget 相同。

<body>
    <div id="wrapper" style="height:200px; background: red">
        <input id="inner" type="button" value="inner" />
    </div>
</body>
<script>
    function func1 (e) {
        console.log(this === e.currentTarget) // true
    }
    function func2 (e) {
        console.log(this === e.currentTarget) // true
    }
    document.getElementById("wrapper").addEventListener('click',func1);
    document.getElementById("inner").addEventListener('click',func2);
</script>

内联事件处理函数中的 this 当代码被内联处理函数调用时,它的this指向监听器所在的DOM元素:

<button onclick="alert(this.tagName.toLowerCase());">
  Show this
</button>
//上面的alert会显示button。注意只有外层代码中的this是这样设置。

<button onclick="alert((function(){return this})());">
  Show inner this
</button>

在这种情况下,没有设置内部函数的 this,所以它指向 global/window 对象(即非严格模式下调用的函数未设置 this 时指向的默认对象)。

5.5闭包

维持了自由变量不被释放的函数, 称为闭包,(自由变量指不在自身上下文,也不在全局上下文中的变量)。 那么闭包函数的特点在哪里,我们知道函数在创建的时候,它的[[scope]]属性就已经确定并不可以改变,所以闭包函数在创建的时候就保存了上级的作用域链,闭包函数通过作用域链去寻找使用到的变量,正常情况下,函数在执行完毕后,将销毁函数的执行上下文,但是由于闭包函数的存在,包含闭包函数的上级函数执行完毕后,如果闭包函数还存在,那么这个上级函数的作用域中的变量仍然保留在内存中供闭包函数访问。 注意:由定义可以知道,闭包函数肯定是定义在函数中,才可能有上级的函数作用域可以访问,否则上级作用域就是全局作用域。全局作用域中的变量本身就一直在内存中,所以访问全局作用域中变量的函数不能称为闭包。

var name = 'global';   
function func1() {
    var name1 = 'func1';
    console.log(name);
    console.log(name);
    function func2() {
        console.log(name);
        var name2 = 'func2';
        function func3() {
            console.log(name2);
        }
        func3();
    }
    func2();
}
func1();

上面代码中,func3为闭包函数,因为它访问了上级函数作用域中的变量name2,func2不能称为闭包函数,因为它们访问的是全局作用域中的变量name。

常见的闭包场景

维持变量的闭包

var person = (function(){
  var _name = 'yangyiliang';
  var _age = 18;
  return {
      getName: function () {
          return _name;
      },
      getAge: function () {
          return _age;
      },
      addAge: function (num) {
          return _age += num;
      },
      reduceAge: function (num) {
          return _age -= num;
      }
  }
})();

console.log(person.addAge(5)); // 23
console.log(person.reduceAge(3)); //20

上面的代码中,首先外层包裹了一个匿名立即执行函数,创造了一个上级函数作用域,getName和getAge方法都是在其内部,并且访问了上级函数作用域中的变量,所以是闭包,所当匿名函数执行完毕后,本该销毁的执行上下文,却因为闭包函数而保留了作用域中的_name和 _age变量, 通过addAge 和reduceAge的结果发现,两个闭包共用保留的作用域。

维持参数的闭包

function makeSizer(size) {
  return function() {
      document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;

上面的代码中,makerSizer函数的返回值即为闭包函数,闭包函数访问上级函数作用域中的参数。2.3 循环创建闭包常见错误

function bind() {
  var arr = document.getElementsByTagName("p");
  for(var i = 0; i < arr.length;i++){
     arr[i].onclick = function(){
          alert(i);
     }
  }
}  
bind();

上面的代码中,假设arr的length为5,想要实现的功能是点击5个P标签分别alert 0,1,2,3,4。但是事实上得到的结果却是都alert 5。 造成这种结果的原因就是绑定的多个onclick函数是闭包函数,他们共同使用保留的上级函数作用域中的变量 i 。 for循环执行结束后 i 的值即为5。 要想解决这种错误,就让这些闭包保存不同的上级作用域即可。

function bind() {
  var arr = document.getElementsByTagName("p");
  for(var i = 0; i < arr.length;i++){
      (function (i) {
          arr[i].onclick = function(){
          alert(i);
          }
      })(i);
  }
}  
bind();

或者

function bind() {
  var arr = document.getElementsByTagName("p");
  for(var i = 0; i < arr.length;i++){
      arr[i].onclick = (function(i){
          return function () {
              alert(i);
          }
      })(i);
  }
}  
bind();

5.6 执行上下文

在JavaScript中,执行上下文(Execution Context)指的是JavaScript代码被执行时所在的环境,包括变量、函数、对象等。执行上下文是一个抽象的概念,但它是理解JavaScript的重要概念之一。 在JavaScript中,执行上下文包括三个重要的组成部分:

  • 1.变量对象(Variable Object):存储当前执行上下文中定义的变量、函数和参数。在全局上下文中,变量对象被称为全局对象,它是全局作用域的根对象。在函数上下文中,变量对象被称为活动对象(Active Object),它是当前作用域的根对象。
  • 2.作用域链(Scope Chain):指的是当前执行上下文中所有父级执行上下文的变量对象的集合,它决定了当前执行上下文中可以访问哪些变量和函数。在函数上下文中,作用域链由当前活动对象和所有父级执行上下文的活动对象的集合组成,它是由JavaScript引擎自动创建的。 作用域链的确定时机是在函数被定义时,也就是在词法分析阶段。当函数被定义时,JavaScript 引擎会创建一个函数对象,同时将函数对象的 [[Scope]] 属性初始化为当前执行上下文的作用域链,也就是函数所在的词法环境的变量对象和上一级执行上下文的作用域链。 在函数执行时,其作用域链就已经确定了,不会再改变。当函数被调用时,会创建该函数的执行上下文对象,并将其作用域链设置为函数对象的 [[Scope]] 属性所指向的链。这样,函数在执行时就可以根据作用域链找到它所需要的变量了。
  • 3.this 值(This Binding):指的是当前执行上下文所在的对象,它在函数上下文中由函数调用方式决定,在全局上下文中为全局对象。在this章节有详细阐述。 JavaScript的执行上下文可以分为全局执行上下文和函数执行上下文两种类型。
    1. 全局执行上下文 全局执行上下文是指在代码执行之前,JavaScript引擎创建的第一个执行上下文,它存在于全局作用域中。在全局执行上下文中,变量对象被称为全局对象,它是全局作用域的根对象,可以通过this关键字在任何地方访问。 全局执行上下文中,作用域链中只包含全局对象,它是由JavaScript引擎在解析和编译JavaScript代码时自动创建的。 全局执行上下文对象可以简化表示为:
globalExecutionContext = {
  VO: globalObject, // 变量对象
  this: globalObject,
  Scope: [globalObject]
};

其中,VO 属性指向全局对象,this 属性也指向全局对象,Scope 属性是一个数组,只包含全局对象。因为全局执行上下文是最外层的执行上下文,所以其作用域链中只包含全局对象。

  • 2.函数执行上下文 函数执行上下文是指在函数被调用时,JavaScript引擎创建的执行上下文,它存在于函数作用域中。在函数执行上下文中,变量对象被称为活动对象,它是当前作用域的根对象,可以通过this关键字访问。 函数执行上下文中,作用域链由当前活动对象和所有父级执行上下文的活动对象的集合组成,它是由JavaScript引擎自动创建的。当函数执行完毕后,JavaScript引擎会销毁函数执行上下文,并释放相应的内存空间。 下面举例说明,假设有如下函数:
function foo(x, y) {
  var z = 10;
  function bar() {}
  
  return x + y + z;
}

当foo函数被调用时,将创建新的函数执行上下文,可表示如下:

// 创建一个执行上下文对象
var fooExecutionContext = {
  // 变量对象
  VO: {
    arguments: {...}, // 包含函数的参数
    x: undefined,
    y: undefined,
    z: undefined,
    bar: function() {...} // 包含函数内部声明的函数
  },
  // this 值
  this: window,
  // 作用域链
  Scope: [
    {...}, // foo 函数自身的变量对象
    {...}, // 全局对象的变量对象
  ]
};

5.7 类型判断

typeof 运算符

在基本数据类型章节,讲到了typeof运算符用于判断一个变量或表达式的数据类型,并返回对应的字符串值。例如:

typeof 42  // 返回 "number"
typeof "hello"  // 返回 "string"
typeof true  // 返回 "boolean"
typeof {}  // 返回 "object"
typeof null  // 返回 "object"
typeof undefined  // 返回 "undefined"
typeof function() {}  // 返回 "function"
typeof Symbol("a")  // 返回 symbol

它只适用于基本数据类型的判断,不能区分undefined和null。在原型链与继承章节讲到了JavaScript中的面向对象编程,typeof也无法区分对象是否是某一个构造函数的实例。

class Person {
  constructor(name) {
    this.name = name;
  }
}

const person = new Person('Alice');
console.log(typeof person); // 输出 "object"

只能判断出是object类型。

instanceof运算符

在 JavaScript 中,instanceof 运算符可以用于判断一个对象是否是某个构造函数的实例,同时也可以判断继承自该构造函数的实例。举例如下:

class Person {
  constructor(name) {
    this.name = name;
  }
}

class Student extends Person {
  constructor(name, grade) {
    super(name);
    this.grade = grade;
  }
}

const person = new Person('Alice');
const student = new Student('Bob', 5);

console.log(person instanceof Person); // 输出 "true"
console.log(student instanceof Person); // 输出 "true"
console.log(student instanceof Student); // 输出 "true"
console.log(person instanceof Student); // 输出 "false"

在上面的代码中,我们定义了一个 Person 类和一个继承自 Person 的 Student 类。然后我们创建了一个 person 对象和一个 student 对象,分别使用 instanceof 运算符判断它们是否是 Person 类和 Student 类的实例。由于 person 对象是直接从 Person 类实例化出来的,因此 person instanceof Person 的结果为 true,而 person instanceof Student 的结果为 false。而 student 对象既是 Student 类的实例,也是 Person 类的实例,因此 student instanceof Person 和 student instanceof Student 都返回 true。 Instance运算符的实现原理如下: 当使用 obj instanceof Constructor 进行检查时,JavaScript 引擎会沿着 obj 的原型链(即 obj 的 proto 属性指向的对象)向上查找,直到找到 Constructor.prototype 或者到达原型链的末端 null。 如果在原型链上找到了 Constructor.prototype,那么 obj 就被认为是 Constructor 的一个实例,instanceof 运算符返回 true;否则,obj 不是 Constructor 的实例,instanceof 运算符返回 false。 需要注意的是,instanceof 运算符只能用于判断对象是否是某个类型的实例,而不能用于判断基本数据类型。 如果让我们自己实现一个instanceof功能的函数 可以按照如下步骤实现:

  • 1.获取对象的原型链(即 obj 的 proto 属性指向的对象),并保存到变量 proto 中。
  • 2.获取构造函数的原型对象(即 Constructor.prototype),并保存到变量 prototype 中。
  • 3.使用一个循环,不断将 proto 设置为它的原型对象(即 proto.proto),直到 proto 为 null 或者和 prototype 相等为止。
  • 4.如果循环结束时,proto 等于 prototype,则说明对象是该构造函数的一个实例,返回 true;否则返回 false。 下面是一个简单的实现示例:
function myInstanceof(obj, Constructor) {
  let proto = Object.getPrototypeOf(obj); // 获取对象的原型链
  let prototype = Constructor.prototype; // 获取构造函数的原型对象
  while (proto !== null) {
    if (proto === prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto); // 将 proto 设置为它的原型对象
  }
  return false;
}

使用上述实现方法,我们就可以手动实现 instanceof 运算符的功能了。

5.8 错误处理机制

捕获错误

try...catch...finally 是 JavaScript 中用于处理异常的一种语法结构,其中 try 代码块中包含可能会抛出异常的代码,catch 代码块用于捕获异常并进行处理,finally 代码块用于指定无论是否发生异常都必须执行的代码。 以下是 try...catch...finally 的基本语法:

try {
  // 可能会抛出异常的代码
} catch (error) {
  // 处理异常的代码
} finally {
  // 无论是否发生异常都会执行的代码
}

当 try 代码块中的代码抛出异常时,JavaScript 引擎会停止执行该代码块,并将异常对象传递给 catch 代码块中的参数 error,然后执行 catch 代码块中的代码。 在 catch 代码块中,我们可以使用 error 参数来获取异常对象的信息,并对异常进行处理。catch 代码块中的代码是可选的,可以省略不写。如果省略了 catch 代码块,那么 finally 代码块会在异常被抛出后立即执行。 finally 代码块中的代码无论是否发生异常都会执行,它常常用于清理资源或恢复状态。finally 代码块也是可选的,可以省略不写。如果省略了 finally 代码块,那么在异常被抛出后,程序会直接跳转到当前作用域的上层作用域。 下面是一个简单的 try...catch...finally 的例子,用来演示如何捕获错误并进行处理:

function divide(x, y) {
  try {
    if (y === 0) {
      throw new Error('Divide by zero');
    }
    return x / y;
  } catch (error) {
    console.error(error);
    return null;
  } finally {
    console.log('Division operation done');
  }
}

console.log(divide(10, 2)); // 输出:Division operation done , 5
console.log(divide(10, 0)); // 输出:Division operation done,Error: Divide by zero,null

分析上面的代码和输出可得知,即使try和catch中进行了return操作,仍然会执行finally中的代码。还需要注意

  • 1.只有在 try 代码块中发生的错误才能被 catch 代码块捕获。如果错误发生在 try 代码块外部,则无法被 catch 代码块捕获。
  • 2.可以使用多个 catch 代码块来处理不同类型的错误。
  • 3.可以在 catch 代码块中重新抛出错误,让上层调用者处理该错误。

抛出错误

在 JavaScript 中,可以使用 throw 语句抛出一个错误,以便在运行时中断程序执行并抛出错误信息。抛出错误可以让开发者更加精细地控制程序的运行过程,及时发现和解决问题,提高代码的健壮性和可靠性。 throw 语句接受一个表达式作为参数,这个表达式可以是任何类型的值,但通常是一个错误对象,例如 Error、TypeError、RangeError 等。 以下是一个使用 throw 语句抛出错误的示例:

function divide(a, b) {
  if (b === 0) {
    throw new Error("除数不能为0");
  }
  return a / b;
}

try {
  let result = divide(10, 0);
  console.log(result);
} catch (error) {
  console.error(error);
}

在上面的示例中,我们定义了一个 divide 函数,用于计算两个数相除的结果。在函数中,我们首先判断除数是否为0,如果是则使用 throw 语句抛出一个错误。然后,在 try 代码块中调用 divide 函数并将结果存储在 result 变量中。如果运行时发生错误,就会进入 catch 代码块并输出错误信息。 当 throw 语句被执行时,JavaScript 引擎会立即停止程序执行,并将控制权交给最近的异常处理器,如果当前作用域没有异常处理器,控制权将传递到上一级作用域的异常处理器。 可以通过自定义错误类型来更精确地描述异常情况。自定义错误类型必须继承自 Error 对象,并可以添加自定义属性和方法,例如:

class MyError extends Error {
  constructor(message, code) {
    super(message);
    this.code = code;
  }

  getCode() {
    return this.code;
  }
}

throw new MyError("自定义错误信息", 500);

在上面的示例中,我们定义了一个自定义错误类型 MyError,它继承自 Error 对象。在 MyError 中添加了一个 code 属性和一个 getCode 方法。当需要抛出自定义错误时,可以使用 throw 语句并传递一个 MyError 对象,例如 throw new MyError("自定义错误信息", 500)。在异常处理器中可以通过 instanceof 关键字判断错误类型,进而做出不同的处理。

错误对象 JavaScript 中的错误对象有一些公共属性,这些属性描述了错误的性质和上下文,包括:

  • 1.name:错误类型的名称,如 "Error"、"TypeError" 等。
  • 2.message:错误的详细描述。
  • 3.stack:包含错误的堆栈跟踪信息。堆栈跟踪是一种追踪代码执行路径的方法,通常在调试和错误报告中使用。 此外,不同类型的错误对象还有各自特定的属性,用于描述特定类型的错误情况。例如:
  • 1.TypeError 对象还具有 expected 和 actual 属性,用于描述类型不匹配的情况下预期和实际的数据类型。
  • 2.RangeError 对象还具有 min 和 max 属性,用于描述数值超出有效范围的情况下的最小和最大值。 这些都很少使用到。 下面列举一些常用的错误类型,这些错误类型都是内置对象它们分别是: Error:通用的错误类型,可以用来创建任何类型的错误。
var error = new Error('Something went wrong.');
throw error;

TypeError:类型错误,例如将非函数类型的对象作为函数调用。

var obj = {};
obj(); // 抛出 TypeError 错误
SyntaxError:语法错误,例如括号不匹配、缺少分号等。
if (x === y {
  console.log("x is equal to y");
}

这段代码中,缺少右括号,会抛出 SyntaxError 错误。 RangeError:范围错误,例如数组越界、递归过深等。

function recursiveFn(num) {
  if (num === 0) {
      return;
  }
  recursiveFn(num - 1);
}

recursiveFn(100000); // 抛出 RangeError 错误

这段代码中,递归函数的深度过深,导致程序栈溢出,会抛出 RangeError 错误。 ReferenceError:引用错误,例如访问不存在的变量或函数。 var a = b + 1; // 抛出 ReferenceError 错误,b 未定义 URIError:URI 错误,例如使用不合法的 URI。 decodeURIComponent('%E0%A4%A'); // 抛出 URIError 错误,不合法的 URI 编码 这些内置的错误类型都是可以被捕获的,使用 try...catch 结构可以对它们进行捕获和处理。

5.9 类

类的概念

类的概念是面向对象编程的一个重要组成部分。面向对象编程强调将程序中的数据和相关操作封装在一个对象中,以便更好地组织和管理代码。类是一种工具,它提供了一种更具可读性、可维护性和可重用性的方式来定义和实现对象。 以下是使用类的一些优点:

  • 1.可重用性:使用类可以轻松地创建多个具有相同属性和方法的对象。这使得代码更容易重用,并且可以减少代码中的重复内容。
  • 2.抽象性:类可以提供抽象的概念和接口,使代码更易于理解和管理。通过隐藏对象的内部细节,类可以简化代码,并提供更高级别的操作。
  • 3.继承性:类允许通过继承来创建新的类。这使得代码更易于扩展,并且可以减少代码的复杂性。
  • 4.封装性:类提供了一种将数据和操作封装在一起的方式。这使得对象更易于管理和维护,并可以更好地保护对象的内部状态。
  • 5.多态性:类允许对象具有不同的行为,即使它们具有相同的接口和方法。这使得代码更灵活,并可以更好地处理复杂的问题。 综上所述,类是一种非常有用的工具,它可以使代码更易于理解、组织和管理,并提供更高级别的操作。它可以提高代码的可重用性、可维护性和可扩展性,并且可以更好地保护和管理对象的状态。

ES5中类的实现

在ES6 没有引入class语法的时候,JavaScript中就是用函数去模拟的类,在原型链和继承章节有讲到,下面看一个例子:

// 定义一个Person类
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log("Hello, my name is " + this.name + " and I am " + this.age + " years old.");
};

// 定义一个Student类,继承自Person类
function Student(name, age, grade) {
  Person.call(this, name, age);
  this.grade = grade;
}

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

Student.prototype.sayHello = function() {
  console.log("Hello, my name is " + this.name + " and I am " + this.age + " years old. I am in grade " + this.grade + ".");
};

// 创建一个Person对象
var person = new Person("John", 30);
person.sayHello(); // 输出 "Hello, my name is John and I am 30 years old."

// 创建一个Student对象
var student = new Student("Jane", 10, 5);
student.sayHello(); // 输出 "Hello, my name is Jane and I am 10 years old. I am in grade 5."

在上面的例子中,我们首先定义了一个Person类,它有两个属性name和age,并有一个sayHello方法。然后,我们定义了一个Student类,它继承自Person类,并添加了一个额外的属性grade。我们使用Person.call(this, name, age)在Student构造函数中调用父类的构造函数,以便继承父类的属性。然后,我们使用Object.create(Person.prototype)来创建一个新的对象,该对象的原型为Person.prototype。最后,我们将Student.prototype.constructor设置为Student,以确保正确的构造函数被调用。 我们还重写了Student类中的sayHello方法,以便输出学生的年级。然后,我们分别创建了一个Person对象和一个Student对象,并调用它们的sayHello方法,以便输出相应的信息。

ES6中的Class类

在ES6中,引入了class关键字,使得创建类和对象更加简单、直观、更符合面向对象的写法。下面用class关键字实现一下上面用ES5写的例子:

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

  sayHello() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

class Student extends Person {
  constructor(name, age, grade) {
    super(name, age);
    this.grade = grade;
  }

  sayHello() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old. I am in grade ${this.grade}.`);
  }
}

// 创建一个Person对象
const person = new Person("John", 30);
person.sayHello(); // 输出 "Hello, my name is John and I am 30 years old."

// 创建一个Student对象
const student = new Student("Jane", 10, 5);
student.sayHello(); // 输出 "Hello, my name is Jane and I am 10 years old. I am in grade 5."

在上面的例子中,我们首先定义了一个Person类,它有两个属性name和age,并有一个sayHello方法。然后,我们使用class关键字定义了一个名为Student的类,它继承自Person类,并添加了一个额外的属性grade。我们使用super关键字调用父类的构造函数,以便继承父类的属性。然后,我们重写了Student类中的sayHello方法,以便输出学生的年级。 我们分别创建了一个Person对象和一个Student对象,并调用它们的sayHello方法,以便输出相应的信息。 构造函数 constructor是JavaScript中的构造函数。在类中使用constructor()方法来定义构造函数,用于在创建对象时进行初始化操作。构造函数会在使用new关键字创建类的实例时自动调用,它可以接受参数,用于初始化实例的属性。在constructor()方法中,我们可以使用this关键字来引用当前实例,并设置实例的属性如下所示:

class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  get area() {
    return this.width * this.height;
  }

  get perimeter() {
    return 2 * (this.width + this.height);
  }
}

const rect = new Rectangle(5, 10);
console.log(rect.area); // 输出 50
console.log(rect.perimeter); // 输出 30

在上面的例子中,我们定义了一个Rectangle类,其中使用constructor()方法来定义构造函数。构造函数接受两个参数width和height,用于初始化实例的width和height属性。在类中,我们还定义了area和perimeter两个getter方法,用于获取矩形的面积和周长。最后,我们使用new关键字创建了一个名为rect的Rectangle实例,并通过调用area和perimeter属性输出了实例的面积和周长。 需要注意的是,一个类只能有一个constructor()方法。如果我们在一个类中定义了多个constructor()方法,JavaScript引擎会抛出SyntaxError错误。

如果在类中没有显式定义constructor()方法,则会自动生成一个默认的constructor()方法。这个默认的constructor()方法没有任何参数,也没有任何初始化操作,但它仍然会被自动调用。下面是一个没有显式定义constructor()方法的例子:

class MyClass {
  sayHello() {
    console.log("Hello World!");
  }
}

const obj = new MyClass();
obj.sayHello(); // 输出 "Hello World!"

在上面的例子中,我们定义了一个名为MyClass的类,其中没有显式定义constructor()方法。但是,当我们使用new关键字创建一个MyClass实例时,JavaScript引擎会自动为我们生成一个默认的constructor()方法,这个方法并没有任何初始化操作。最后,我们通过调用sayHello()方法输出了实例的属性。 需要注意的是,如果我们需要在类中进行初始化操作,那么就必须显式定义constructor()方法,否则实例的属性就会保持默认值。

如果在constructor()方法中显式地返回一个对象时,返回的对象会替换掉实例化的对象,下面是一个示例代码,用于演示在constructor()方法中显式返回一个对象的情况:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    return {message: "This is an object returned from constructor"};
  }
}

const person = new Person("Alice", 30);
console.log(person); // 输出 {message: "This is an object returned from constructor"}
console.log(person instanceof Person); // 输出 false

在上面的代码中,我们定义了一个Person类,其中在constructor()方法中显式地返回了一个对象{message: "This is an object returned from constructor"}。在实例化Person类时,返回的对象被赋值给了person变量。最后,我们输出了person变量的值,并使用instanceof运算符判断person是否为Person类的实例,发现person不是Person类的实例。 实例属性 实例属性是定义在类的实例上的属性。每个类实例都会拥有自己的一份实例属性,它们不会被其他实例共享。

    1. 构造函数中this设置实例属性,例如:
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

// 创建一个名为 john 的 Person 实例对象
const john = new Person('John', 25);
// 创建一个名为 sarah 的 Person 实例对象
const sarah = new Person('Sarah', 30);

// 获取 john 的 name 属性值
console.log(john.name); // 输出: 'John'

// 修改 sarah 的 age 属性值
sarah.age = 32;

// 获取 sarah 的 age 属性值
console.log(sarah.age); // 输出: 32
    1. Class Field声明实例属性 ES2022 引入了 Class Field 声明实例属性的语法,可以通过在类的最顶层直接声明实例属性。例如:
class Person {
  name = '';
  age = 0;
}

它的优点:

  • 1 更加简洁。声明实例属性的代码可以直接写在类的定义中,不需要在构造函数中单独处理。
  • 2 更加可读。将实例属性声明在类的定义中,可以更加清晰地显示类有哪些属性,更加方便阅读和理解类的定义。 同样也可以在构造函数中继续用this访问这些属性和给其赋值,例如:
class Person {
  name = '';
  age = 0;

  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

访问器属性

在 ES6 之后,类的内部可以使用 get 和 set 关键字来定义访问器属性(accessor property),以便更加灵活地控制属性的读取和写入。下面是一个使用 get 和 set 定义属性的示例:

class Person {
  constructor(name) {
    this._name = name;
  }

  get name() {
    return this._name;
  }

  set name(newName) {
    this._name = newName;
  }
}

const person = new Person('Alice');
console.log(person.name); // 输出 'Alice'
person.name = 'Bob';
console.log(person.name); // 输出 'Bob'

访问器属性,也称为存取器属性,是 JavaScript 对象中的一种特殊类型的属性。与数据属性不同,访问器属性并不是保存一个值,而是定义一个函数,这个函数被调用时会返回一个值。访问器属性通常用于对一个对象的属性进行读取和设置时进行特殊操作,比如输入验证、计算新值等。在类的内部,可以使用get和set关键字来创建访问器属性。get关键字用于获取属性值,set关键字用于设置属性值。 访问器属性并不是实例属性,它们是定义在类的原型对象上的属性。当我们访问一个实例的访问器属性时,实际上是调用了原型对象上的getter或setter方法。因此,访问器属性是定义在原型对象上的属性,而不是实例对象上的属性。 访问器属性一般用于控制类的属性访问行为,例如:

  • 1 对属性的读取和写入进行限制,以保护类的数据不受非法访问。
  • 2 在读取和写入属性时进行特殊的逻辑处理,例如计算属性的值,数据格式转换等等。
  • 3 实现一些与属性相关的特殊方法,例如属性的计数器、缓存等等。 另外,访问器属性也常常被用于一些面向对象的编程模式中,例如实现属性的延迟加载(lazy-loading)、属性的拦截(interception)等等。 例如,我们可以在一个Person类中定义一个访问器属性fullName,该属性的值由firstName和lastName拼接而成,当设置fullName时,将自动更新firstName和lastName的值。示例代码如下:
class Person {
  constructor(firstName, lastName) {
    this._firstName = firstName;
    this._lastName = lastName;
  }

  get fullName() {
    return this._firstName + ' ' + this._lastName;
  }

  set fullName(name) {
    const names = name.split(' ');
    this._firstName = names[0];
    this._lastName = names[1];
  }
}

const person = new Person('John', 'Doe');
console.log(person.fullName); // John Doe

person.fullName = 'Jane Smith';
console.log(person.fullName); // Jane Smith

console.log(person._firstName); // John
console.log(person._lastName); // Doe

在这个例子中,fullName属性是一个访问器属性,用于读取和设置Person实例的全名,其实际值由firstName和lastName拼接而成。在类的构造函数中,我们使用了下划线前缀来定义了firstName和lastName的实例属性,以示这些属性是“私有”的。在fullName的set方法中,我们解析了传入的字符串参数,分别将其赋值给firstName和lastName,并更新了实例属性的值。在读取fullName属性时,实际上是调用了fullName的get方法,该方法返回了firstName和lastName的拼接结果。

原型属性和方法

在原型链和继承章节我们讲述了什么是原型对象,用ES5的写法是构造函数的prototype对象,例如: Person.prototype.species = "human"; 这样给原型上添加属性,一般情况下我们都会给原型上添加方法而不是属性,因为方法在实例中共享是常见的情况,而属性值在实例中共享是很少见的情况。在ES6中Class的最顶层声明的方法就会是原型上的方法,而原型属性的声明还是要通过prototype对象,例如:

class MyClass {
  constructor() {
    // 构造函数
  }
  // 原型方法
  myMethod() {
    // ...
  }
}
// 原型属性
MyClass.prototype.myProperty = 123;

静态属性

静态属性指的是类本身的属性,而不是类的实例的属性。静态属性可以在类的内部定义,使用static关键字声明,也可以在类的外部定义。 使用静态属性可以在类级别上保存状态或值,而不是每个实例上保存。这样做可以节省内存,同时也方便在类级别上管理数据。 静态属性可以通过类名直接访问,而不需要创建类的实例。可以使用静态属性来定义常量、默认值等。 以下是一个使用静态属性的例子:

class MyClass {
  static staticProperty = 'static property';
  instanceProperty = 'instance property';
}

在上面的例子中,staticProperty是一个静态属性,可以通过MyClass.staticProperty直接访问。instanceProperty是一个实例属性,需要通过类的实例访问。 注意,在类的内部访问静态属性时需要使用this.constructor来引用类本身。例如:

class MyClass {
  static staticProperty = 'static property';
  
  getStaticProperty() {
    return this.constructor.staticProperty;
  }

  static staticMethod() {
    console.log('this is a static method')
  }
}

const instance = new MyClass()
console.log(instance.getStaticProperty()) // static property
coneole.log(MyClass.staticProperty) // static property
console.log(MyClass.staticMethod()) // this is a static method

在上面的例子中,getStaticProperty方法中使用了this.constructor来引用类本身,以便访问静态属性,staticMethod是静态的方法,可以直接通过类调用。 其实在类的内部用static关键字声明和直接给这个类对象赋值的效果是一样的,但是用static关键字在类的内部写,可读性更强。例如,上边的例子也可以用下边的方式实现:

class MyClass {
  getStaticProperty() {
    return this.constructor.staticProperty;
  }
}

MyClass.staticProperty = 'new static property'
MyClass.staticMethod = function () {
  console.log('this is a static method')
}

const instance = new MyClass()
console.log(instance.getStaticProperty()) // static property
coneole.log(MyClass.staticProperty) // static property
console.log(MyClass.staticMethod()) // this is a static method

私有属性

ES2021 引入了私有字段(Private Fields)和私有方法(Private Methods),用 # 符号进行标识。私有字段和方法只能在类的内部访问,外部无法访问,这样可以更好地保护类的数据和方法,避免被外部非法修改或调用。私有字段和方法的定义方式如下:

class MyClass {
  #privateField; // 私有字段

  #privateMethod() { // 私有方法
    // do something
  }

  publicMethod() {
    this.#privateField = 'foo'; // 在类内部可以访问私有字段
    this.#privateMethod(); // 在类内部可以调用私有方法
  }
}

下面是一个示例:

class Person {
  #name = '';
  #age = 0;
  #calculateBirthYear() {
    const currentYear = new Date().getFullYear();
    return currentYear - this.#age;
  }

  constructor(name, age) {
    this.#name = name;
    this.#age = age;
  }

  getBirthYear() {
    return this.#calculateBirthYear();
  }
}

const person = new Person('John', 30);
console.log(person.#name); // SyntaxError: Private field '#name' must be declared in an enclosing class
console.log(person.getBirthYear()); // output: 1991
console.log(person.#calculateBirthYear()); // SyntaxError: Private field '#calculateBirthYear' must be declared in an enclosing class

在这个例子中,我们使用 Private Fields 和 Methods 定义了两个私有属性 #name 和 #age,以及一个私有方法 #calculateBirthYear。在类的构造函数中,我们使用这些私有属性进行初始化。我们还定义了一个公有方法 getBirthYear,它调用了私有方法 #calculateBirthYear。通过在实例上调用 getBirthYear 方法,我们可以获得这个人的出生年份,但是我们无法直接访问私有属性和方法,即使是通过实例也不行,这就保证了私有性。

类的继承

在原型链和继承章节我们讲解了继承的原理,和ES5中如何实现的继承,这里讲一下ES6的Class语法中关键字extends来实现继承。子类使用extends关键字继承父类,然后可以使用super关键字来调用父类的构造函数和方法。和ES5中继承的区别是,Class的继承还能继承父类的静态属性和方法。看下面的例子:

class Animal {
  constructor(name) {
    this.name = name;
  }

  static hello() {
    console.log('Animal hello');
  }

  eat() {
    console.log(`${this.name} is eating.`);
  }
}

class Cat extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  static goodbye() {
    console.log('Cat goodbye');
  }

  meow() {
    console.log(`${this.name} (${this.breed}) is meowing.`);
  }
}

const fluffy = new Cat('Fluffy', 'Persian');
fluffy.eat(); // "Fluffy is eating."
fluffy.meow(); // "Fluffy (Persian) is meowing."

Cat.hello(); // "Animal hello"
Cat.goodbye(); // "Cat goodbye"

在这个例子中,Cat 类继承了 Animal 类,可以访问 Animal 类的实例方法 eat() 和静态方法 hello()。同时,Cat 类也定义了自己的实例方法 meow() 和静态方法 goodbye()。在实例化 Cat 类时,可以看到实例对象 fluffy 可以调用 eat() 和 meow() 方法。而 Cat 类也可以直接调用 hello() 和 goodbye() 静态方法。

5.10遍历器(Iterator)

遍历器提供了一种统一的访问集合对象元素的方式,让开发者可以用同样的方式去遍历不同类型的数据结构。在ES6之前,JavaScript中遍历集合对象的方式比较繁琐,需要使用for循环或者forEach方法,而且对于不同的集合对象,还需要使用不同的方法进行遍历,不够统一和方便。使用遍历器之后,开发者可以更方便地遍历集合对象。遍历器机制首先是需要可遍历的数据结构实现Iterator 接口,它是需要数据结构具有 Symbol.iterator 属性,该属性对应的值必须是一个函数,该函数返回一个迭代器对象。该迭代器对象实现了 next() 方法,用于按顺序访问迭代器的每个成员。然后提供了一个新的for…of语法消费迭代器对象进行遍历。 实现 Iterator 接口需要定义一个对象的Symbol.iterator属性,该属性值为一个函数,函数返回一个迭代器对象。迭代器对象包含一个next方法,该方法每次调用返回一个包含value和done属性的对象,其中value属性表示下一个元素的值,done属性表示迭代器是否已经到达结尾。 下面是一个自定义迭代器的示例代码,该迭代器可以遍历一个包含姓名和年龄信息的对象数组:

let persons = [
  { name: 'Alice', age: 21 },
  { name: 'Bob', age: 22 },
  { name: 'Charlie', age: 23 }
];

let personIterator = {
  [Symbol.iterator]: function() {
    let index = 0;
    let personsLength = persons.length;
    return {
      next: function() {
        if (index < personsLength) {
          let result = { value: persons[index], done: false };
          index++;
          return result;
        } else {
          return { done: true };
        }
      }
    };
  }
};

for (let person of personIterator) {
  console.log(person.name, person.age);
}

该示例中,定义了一个包含Symbol.iterator属性的对象personIterator,该属性的值是一个函数,该函数返回一个包含next方法的迭代器对象。next方法返回一个对象,其中value属性为数组中的一个元素,done属性表示是否已经到达数组的结尾。使用for...of循环遍历personIterator对象时,每次迭代将输出数组中的一个元素,包含姓名和年龄信息。 Iterator 的遍历过程一般是通过 for...of 循环来实现的。for...of 循环在每次迭代时,都会自动调用迭代器对象的 next() 方法,将返回的值赋给变量。 具体的遍历过程如下:

  • 1 调用可迭代对象的 Symbol.iterator 方法,返回一个迭代器对象;
  • 2 调用迭代器对象的 next() 方法,返回一个包含 value 和 done 两个属性的对象;
  • 3 如果 done 属性为 true,则遍历结束,否则执行下一步;
  • 4 将 value 属性赋值给变量,继续执行第 2 步,直到遍历结束。 需要注意的是,迭代器对象每次迭代都会返回一个新的值,所以迭代器对象的遍历过程是不可逆的,也就是说不能重复遍历。如果需要重新遍历,则需要重新调用可迭代对象的 Symbol.iterator 方法获取一个新的迭代器对象。 JavaScript 中实现遍历器的数据结构包括 Array、Map、Set、String、TypedArray、arguments、NodeList 等。实现了 Symbol.iterator 接口的数据结构都可以使用 for...of 循环进行遍历操作。