js 彻底理解原型链

153 阅读4分钟

js中new一个函数结果返回一个对象,这会混淆大家对原型链的理解

1.先弄清楚原型链是什么

原型链设计就是为了让new出来的对象有构造函数的属性

直接看看new做了什么

function myNew(func, ...args) {
  const obj = {};     // 新建一个空对象
  const result = func.call(obj, ...args);  // 执行构造函数
  obj.__proto__ = func.prototype;    // 设置原型链

  // 注意如果原构造函数有Object类型的返回值,包括Functoin, Array, Date, RegExg, Error
  // 那么应该返回这个返回值
  const isObject = typeof result === 'object' && result !== null;
  const isFunction = typeof result === 'function';
  if(isObject || isFunction) {
    return result;
  }

  // 原构造函数没有Object类型的返回值,返回我们的新对象
  return obj;
}

function Puppy(age) {
  this.puppyAge = age;
}

Puppy.prototype.say = function() {
  console.log("汪汪汪");
}

const myPuppy3 = myNew(Puppy, 2);

console.log(myPuppy3.puppyAge);  // 2
console.log(myPuppy3.say());     // 汪汪汪

new会返回一个对象,如果构造函数没有返回Object类型的返回值,会返回一个新对象这个对象继承了构造函数的属性

2.函数的继承

js设计函数有两个默认的属性prototype和__proto__,object则有__proto__ 这个__proto__就是为了找构造函数的属性object.proto = Func.prototype

function Parent() {}
function Child() {}

Child.prototype.__proto__ = Parent.prototype;

const obj = new Child();
console.log(obj instanceof Child );   // true
console.log(obj instanceof Parent );   // true

上述继承方法只是让Child访问到了Parent原型链,但是没有执行Parent的构造函数。

function Parent() {
  this.parentAge = 50;
}
function Child() {}

Child.prototype.__proto__ = Parent.prototype;

const obj = new Child();
console.log(obj.parentAge);    // undefined

看new方法知道会执行Child的构造方法,Father的parentAge不会赋值给this,new出来的child没有parentAge,但是你用obj.parentAge是可以访问到的。

为了解决这个问题,我们不能单纯的修改Child.prototype.__proto__指向,还需要用new执行下Parent的构造函数:

function Parent() {
  this.parentAge = 50;
}
function Child() {}

Child.prototype.__proto__ = new Parent();

const obj = new Child();
console.log(obj.parentAge);    // 50

上述方法会多一个__proto__层级,可以换成修改Child.prototype的指向来解决,注意将Child.prototype.constructor重置回来:

function Parent() {
  this.parentAge = 50;
}
function Child() {}

Child.prototype = new Parent();
Child.prototype.constructor = Child;      // 注意重置constructor

const obj = new Child();
console.log(obj.parentAge);    // 50

当然还有很多其他的继承方式,他们的原理都差不多,只是实现方式不一样,核心都是让子类拥有父类的方法和属性,感兴趣的朋友可以自行查阅。

3.instanceof

知道了原理,其实我们也知道了instanceof是干啥的。instanceof不就是检查一个对象是不是某个类的实例吗?换句话说就是检查一个对象的的原型链上有没有这个类的prototype,知道了这个我们就可以自己实现一个了:

function myInstanceof(targetObj, targetClass) {
  // 参数检查
  if(!targetObj || !targetClass || !targetObj.__proto__ || !targetClass.prototype){
    return false;
  }

  let current = targetObj;

  while(current) {   // 一直往原型链上面找
    if(current.__proto__ === targetClass.prototype) {
      return true;    // 找到了返回true
    }

    current = current.__proto__;
  }

  return false;     // 没找到返回false
}

// 用我们前面的继承实验下
function Parent() {}
function Child() {}

Child.prototype.__proto__ = Parent.prototype;

const obj = new Child();
console.log(myInstanceof(obj, Child) );   // true
console.log(myInstanceof(obj, Parent) );   // true
console.log(myInstanceof({}, Parent) );   // false

5.总结

  1. JS中的函数可以作为函数使用,也可以作为类使用
  2. 作为类使用的函数实例化时需要使用new
  3. 为了让函数具有类的功能,函数都具有prototype属性。
  4. 为了让实例化出来的对象能够访问到prototype上的属性和方法,实例对象的__proto__指向了类的prototype。所以prototype是函数的属性,不是对象的。对象拥有的是__proto__,是用来查找prototype的。
  5. prototype.constructor指向的是构造函数,也就是类函数本身。改变这个指针并不能改变构造函数。
  6. 对象本身并没有constructor属性,你访问到的是原型链上的prototype.constructor
  7. 函数本身也是对象,也具有__proto__,他指向的是JS内置对象Function的原型Function.prototype。所以你才能调用func.call,func.apply这些方法,你调用的其实是Function.prototype.callFunction.prototype.apply
  8. prototype本身也是对象,所以他也有__proto__,指向了他父级的prototype__proto__prototype的这种链式指向构成了JS的原型链。原型链的最终指向是Object的原型。Object上面原型链是null,即Object.prototype.__proto__ === null
  9. Function.__proto__ === Function.prototype。这是因为JS中所有函数的原型都是Function.prototype,也就是说所有函数都是Function的实例。Function本身也是可以作为函数使用的----Function(),所以他也是Function的一个实例。类似的还有ObjectArray等,他们也可以作为函数使用:Object(), Array()。所以他们本身的原型也是Function.prototype,即Object.__proto__ === Function.prototype。换句话说,这些可以new的内置对象其实都是一个类,就像我们的Puppy类一样。

再来看一下完整图:

image-20200222160832782

看重点【Function.__proto__ === Function.prototype

希望可以帮到你理解原型链!