JS原型,继承,new

55 阅读5分钟

1,一些基本的概念

  1. 隐式原型和显示原型
    1. 所有的实例对象, 都有隐式原型属性__proto__
    2. 所有的构造函数,都有显示原型属性prototype
    3. 他们指向同一个对象, 这个对象就是原型对象
    4. 构造函数.prototype === 实例对象.__proto__
const obj = {}
const arr = []
const fun = function(){}
console.log(obj.__proto__ === Object.prototype) // true
console.log(arr.__proto__ === Array.prototype) // true
console.log(fun.__proto__ === Function.prototype) // true
  1. constructor:原型对象上的constructor属性是指向构造函数
const obj = {}
console.log(obj.__proto__.constructor); // ƒ Object() { [native code] }
function Person() {}
const p = new Person()
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__.constructor === Person) // true;

2,原型链

js原型链,是一种在对象上查找属性和方法的规则。

  1. 查找属性和方法时,先在自身查找,如果有,返回并结束。
  2. 如果自身上没有该属性或方法,到其隐式原型(proto)上查找,如果有,返回并结束。
  3. 如果原型上也没有,则去原型的原型上查找,以此类推,直到原型链的终点。如果都没有找到,返回undefined
  4. 原型链的终点,就是Object构造函数的prototype。即Object.prototype。它的隐式原型__proto__为null
console.log(Object.prototype.__proto__); // null
  1. Function函数是所有函数(包括自身)的构造函数, 所有函数的隐式原型, 都和Function的显示原型指向同一个对象
const fun = function(){}
fun.__proto__ === Function.prototype // true
Function.__proto__ === Function.prototype // true
  1. Function的prototype,这个对象的隐式原型又被JS自动指向Object的原型
Function.prototype.__proto__ === Object.prototype // true
  1. Object.hasOwnProperty可以查看对象自身上是否有某个属性, 而不去原型链上查找
const obj = {}
console.log(obj.toString()); // [object Object]
console.log(obj.hasOwnProperty('toString')); // false

3,原型链总结

  • 原型就是JS查找属性的一套规则
  • 所有的实例对象, 都有隐式原型属性__proto__, 所有的构造函数有显示原型属性prototype, 他们指向同一个对象, 这个对象就是原型对象
  • 原型对象上又有constructor属性指回构造函数
  • 在查找对象上的属性的时候, 会先在自身查找, 如果没有就到原型上查找, 还没有就到原型的原型上查找, 直到找到Object的原型对象上, 如果仍然没有, 就返回undefined
  • 原型链的终点就是Object的原型对象, 它没有的隐式原型指向了null
  • Function函数是所有函数(包括自身)的构造函数, 所有函数的隐式原型, 都和Function的显示原型指向同一个对象, 这个对象的隐式原型, 又被JS自动指向Object的原型
  • Object.hasOwnProperty可以查看对象自身上是否有某个属性, 而不去原型链上查找

4,关于__proto__的补充

  • __proto__现在已经不被推荐使用,在文档中使用[[Prototype]]对其进行描述
  • 如果要读取__proto__,推荐使用Object.getPrototypeOf(obj) 返回对象 obj 的 [[Prototype]]
function Parent(name, age){
  this.name = name
  this.age = age
}
const p = new Parent('zhangsan', 38)
console.log(p.__proto__ === Object.getPrototypeOf(p)); // true
console.log(Object.getPrototypeOf(p) === Parent.prototype) // true
  • Object.setPrototypeOf(obj, proto) —— 将对象 obj 的 [[Prototype]] 设置为 proto。
function Parent(name, age){
  this.name = name
  this.age = age
}
const p = new Parent('zhangsan', 38)
Object.setPrototypeOf(p, {
  say(){
    console.log('say')
  }
})
p.say() // say
console.log(Object.getPrototypeOf(p) === Parent.prototype) // false
  • Object.create(proto, [descriptors]) —— 利用给定的 proto 作为 [[Prototype]] 和可选的属性描述来创建一个空对象。
const person = {
  name: 'zhangsan',
  age: 18
}
const p = Object.create(person, {
  say: {
    value: function(){
      console.log('say')
    }
  },
  address: {
    value: 'beijing'
  }
})
console.log(p) // {address: 'beijing', say: ƒ}
p.say() // say
console.log(p.address); // beijing
console.log(Object.getPrototypeOf(p) === person); // true

5,继承

原型链继承

  • 父类的实例作为子类的原型对象
  • 易于实现,核心就是 Children.prototype = new Parent()
  • 缺点
    • 创建子类实例时,不能传参
function Parent(name, age){
  this.name = name
  this.age = age
}
Parent.prototype.say = function(){
  console.log('hello')
}
function Children(){}
Children.prototype = new Parent()
const child = new Children()
child.say() // hello

借用构造函数继承

  • 在子类中通过call调用父类,实现传参,解决了传参和共享父类引用属性的问题
  • 可以在创建时传参,但是不能使用父类的方法
function Parent(name, age){
  this.name = name
  this.age = age
}
function Children(name, age){
  Parent.call(this, name, age)
}

组合式继承

  • 将上面两种方式结合
  • 可以传参
  • 可以复用方法
function Parent(name, age){
  this.name = name
  this.age = age
}
function Children(name, age){
  Parent.call(this, name, age)
}
Children.prototype = new Parent()
Children.prototype.constructor = Children

组合寄生式继承

  • 创建一个用于封装继承过程的函数, 在函数中对父类的实例对象进行增强之后, 再指定为子类的原型
function Parent(name, age){
  this.name = name
  this.age = age
}
function Children(name, age, address){
  Parent.call(this, name, age)
  this.address = address
}
function inherit(Children, Parent){
  // 创建一个父类的实例对象,将这个实例对象赋值给子类的原型对象
  const obj = new Parent()
  obj.constructor = Children
  Children.prototype = obj
  // 对子类的原型对象进行扩展
  obj.say = function(){
    console.log('say')
  }
}
inherit(Children, Parent)
const c = new Children('zhangsan', 18, 'beijing')
console.log(c)
c.say()
  • inherit方法还有可优化的地方,就是不必实际创建父类实例,而只要能获取父类原型就可以
function inherit(Children, Parent){
  function F(){}
  F.prototype = Parent.prototype
  const obj = new F()
  Children.prototype = obj
  obj.constructor = Children
  // 对子类的原型对象进行扩展
  obj.say = function(){
    console.log('say')
  }
}
  • 还可以再次通过Object.create()进行修改

6,new操作符

new

new操作符用于实例化,他内部做了以下事情:

  1. 创建一个空的对象newInstance
  2. 如果构造函数的prototype为一个对象,则将newInstance的__proto__指向构造函数的prototype,否则newInstance将保持为一个普通的对象,__proto__指向Object.prototype
  3. 使用给定参数执行构造函数,并将newInstance绑定为this上下文
  4. 如果构造函数返回非原始值,则该返回值成为整个new表达式的结果。否则返回newInstance

我们来模仿它的行为,实现一个自己的new

function MyNew(constructor, ...args){
  const newInstance = {} // 创建一个新对象
  if (constructor.prototype !== null && typeof constructor.prototype === 'object') {
    newInstance.__proto__ = constructor.prototype
  }
  const result = constructor.apply(obj, args) // 通过apply调用构造函数, 并将参数传递进去,改变this指向
  return result instanceof Object ? result : obj // 返回一个对象,如果构造函数返回的是对象,则返回构造函数返回的对象,否则返回新创建的对象
}

new.target

  • 函数有两种调用方式:直接调用,作为构造函数被调用(new)
  • 当作为构造函数被调用时,new.target指向被new执行的构造函数
  • 当直接调用时,new.target为undefined
  • 可以通过new.target判断函数的调用方式
function Foo() {
  if (!new.target) throw "Foo() must be called with new";
  console.log("Foo instantiated with new");
}

Foo(); // throws "Foo() must be called with new"
new Foo(); // logs "Foo instantiated with new"