# JS

# typeof

其中数组、对象、null都会被判断为object,其他判断都正确。

# instanceof

instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型

instanceof只能正确判断引用数据类型,而不能判断基本数据类型。

# 0.1 + 0.2 !== 0.3

parseFloat((0.1 + 0.2).toFixed(1)) === 0.3 true

# Object.is() 与比较操作符 “===”、“==” 的区别?

  • 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
  • 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。

# || 和 && 操作符的返回值?

|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。

  • 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
  • && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。

|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果

# 闭包

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

本质就是上级作用域内变量的生命周期,因为被下级作用域内引用,而没有被释放。就导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放。

# ES5、ES6 继承

彻底弄清js继承的几种实现方式 (opens new window)

# 1. 原型链继承

原型链继承是最基本的继承方式,通过将子类的原型设置为父类的实例来实现继承。

示例代码:

function Parent() {
  this.name = 'fedaily'
}

Parent.prototype.getName = function() {
  return this.name;
}

function Child() {}

// 这里也可以直接写出Child.prototype = Parent.prototype
// 但是这样就不能访问到父类的构造函数的属性了,即this.name
Child.prototype = new Parent()

var child = new Child()
console.log(child.getName()) // fedaily

缺点

父类构造函数中的引用类型(比如对象/数组),会被所有子类实例共享。其中一个子类实例进行修改,会导致所有其他子类实例的这个值都会改变

# 2. 构造函数继承

构造函数继承其实就是通过修改父类构造函数this实现的继承。我们在子类构造函数中执行父类构造函数,同时修改父类构造函数的this为子类的this。

我们直接看如何实现:

function Parent() {
  this.name = ['fedaily']
}

function Child() {
  Parent.call(this)
}

var child = new Child()
child.name.push('fe')

var child2 = new Child() // child2.name === ['fedaily']

优点

解决了原型链继承中构造函数引用类型共享的问题,同时可以向构造函数传参(通过call传参)

缺点

所有方法都定义在构造函数中,每次都需要重新创建(对比原型链继承的方式,方法直接写在原型上,子类创建时不需要重新创建方法)

# 3. 组合继承

同时结合原型链继承、构造函数继承就是组合继承了。

function Parent() {
  this.name = 'fedaily'
}

Parent.prototype.getName = function() {
  return this.name
}

function Child() {
  Parent.call(this)
  this.topic = 'fe'
}

Child.prototype = new Parent()
// 需要重新设置子类的constructor,Child.prototype = new Parent()相当于子类的原型对象完全被覆盖了
Child.prototype.constructor = Child

优点

同时解决了构造函数引用类型的问题,同时避免了方法会被创建多次的问题

缺点

父类构造函数被调用了两次。同时子类实例以及子类原型对象上都会存在name属性。虽然根据原型链机制,并不会访问到原型对象上的同名属性,但总归是不美。

# 4. 寄生组合继承

寄生组合继承其实就是在组合继承的基础上,解决了父类构造函数调用两次的问题。我们来看下如何解决的:

function Parent() {
  this.name = 'fedaily'
}

Parent.prototype.getName = function() {
  return this.name
}

function Child() {
  Parent.call(this)
  this.topic = 'fe'
}

// 仔细看这个函数的实现
inherit(Child, Parent)
function inherit(child, parent) {
  var prototype = object(parent.prototype)
  prototype.constructor = child
  child.prototype = prototype
}

// 这个函数的作用可以理解为复制了一份父类的原型对象
// 如果直接将子类的原型对象赋值为父类原型对象
// 那么修改子类原型对象其实就相当于修改了父类的原型对象
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

优点

这种方式就解决了组合继承中的构造函数调用两次,构造函数引用类型共享,以及原型对象上存在多余属性的问题。是推荐的最合理实现方式(排除ES6的class extends继承哈哈哈)。

缺点

没有啥特别的缺点

# 5. 使用 Object.create 方法

function Parent(name) {
  this.name = name;
}

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

function Child(name, age) {
  Parent.call(this, name); // 调用父类构造函数
  this.age = age;
}

// 使用 Object.create 设置原型链
Child.prototype = Object.create(Parent.prototype);

// 修复构造函数指向
Child.prototype.constructor = Child;

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

var child = new Child('Alice', 20);
child.sayName(); // Alice
child.sayAge();  // 20

# 6. ES6继承

ES6 提供了 class 语法糖,同时提供了 extends 用于实现类的继承。这也是项目开发中推荐使用的方式。

使用 class 继承很简单,也很直观:

class Parent {
  constructor() {
    this.name = 'fedaily'
  }

  getName() {
    return this.name
  }
}

class Child extends Parent {
  constructor() {
    // 这里很重要,如果在this.topic = 'fe'后面调用,会导致this为undefined,具体原因可以详细了解ES6的class相关内容,这里不展开说明
    super()
    this.topic = 'fe'
  }
}

const child = new Child()
child.getName() // fedaily

# 总结

  • 原型链继承:简单但不能传递参数,且所有实例共享父类的属性。
  • 构造函数继承:可以传递参数,但不能继承父类的原型方法。
  • 组合继承:结合了原型链和构造函数的优点,但会调用两次父类构造函数。
  • 寄生组合继承:解决了组合继承的多次调用问题,是最常用的继承方式。
  • 使用 Object.create 方法:简洁且避免了调用父类构造函数的问题。

# 原型 & 原型链

上次更新: 11/6/2024, 4:10:52 PM