细说javascript的constructor和prototype

写多了 react 是否是已经忘记了什么是原型链了,是否是已经忘记了那个纯真骚年的那份初心了,前端,写多了高大上的代码,是否是应该静下心来,好好学习下基础,下面慢慢回溯下几个常识点:前端

  • constructor 构造器
  • prototype 原型链
  • new 一个对象到底发生了什么

constructor 构造器

constructor 是每一个实例对象都会拥有的一个属性,并且这个属性的实在乎义在于一个指针,它指向了建立当前这个实例对象的类。react

function Person() {}
let p = new Person();
// ƒ Person() {}
console.log(p.constructor);
复制代码

控制台打印结果能够看出,p.constructor 指向的是 Person 对象,后面会详解 new 的过程。es6

constructor 的属性值是能够随时改变的,若是不赋值,那就默认指向了建立这个实例对象的类,若是赋值了,那就会指向所赋值。api

在通常开发中,咱们是否是不多用到这个属性啊,下面我就上点干货,来看看 Preact 源码里是怎么使用这个属性来解决业务场景的。数组

Preact 组件有两种建立方式,一种是利用类建立,继承 Preact.Component 父类或者不继承,拥有这个父类的 render 方法等属性,另外一种是经过 function 建立的无状态组件(PFC),下面我就来讲下 Preact 中是怎么使用 constructor 属性来处理的。babel

  • 建立一个无状态组件
// 函数建立的无状态组件
const Foo = () => {
  return <div>Foo</div>;
};

// 常见的容器组件建立方式
class App extends Preact.Component {
  render() {
    return (
      <div> <Foo /> </div>
    );
  }
}
复制代码
  • babel 转码
// 上述组件通过babel后转码后的虚拟dom生成函数
Preact.createElement(
  "div",
  null,
  React.createElement("p", null, "hahahaha"),
  React.createElement(Foo, null)
);

// 该函数返回的是一个虚拟dom
var Foo = function Foo() {
  return Preact.createElement("div", null, "Foo");
};
复制代码
  • 虚拟 dom 中的类型判断
if(typeof type === 'function'){
    ...
}
复制代码

上述代码中,Preact.createElement 方法中的第一个参数就是 type,其中 Foo 就是 function 类型。dom

  • Foo 函数的两种形式
if (Foo.prototype && Foo.prototype.render) {
}
复制代码

在代码中会判断 Foo 函数是否能访问 render 方法,首次渲染确定是没有的,全部,上述的判断会断定 false,关键点来了,下面来看看若是处理的:函数

首先来看下 Preact.Component 代码的实现:oop

function Component(props, context) {
  this.context = context;
  this.props = props;
  this.state = this.state || {};
  // ...
}
Object.assign(Component.prototype, {
  setState(state, callback) {},
  forceUpdate(callback) {},
  render() {}
});
复制代码

能够看出,若是是容器组件,继承了父类 Preact.Component ,就可以访问 render 方法,那么若是是无状态组件,怎样让这个组件拥有 render 方法:学习

let inst = new React.Component(props, context);
inst.constructor = Foo;
inst.render = function(props, state, context) {
  return this.constructor(props, context);
};
复制代码

起初看这个寥寥几行代码,包含了很多细致的东西。

首先,它定义了 Preact.Component 这个类的实例对象 inst,此时,这个 instconstructor 默认指向 Preact.Component 这个类,接下来,给 instconstructor 这个属性赋值了,改变指向函数 Foo,最后给这个实例对象 inst 添加一个 render 方法,核心就在这个方法,这个方法执行了 this.constructor ,其实就是执行了 Foo 方法,而 Foo 方法最终返回的就是一个虚拟 dom。

如今就说通了,其实,无状态组件最终也会拥有一个 render 方法,触发后会返回一个虚拟 dom 或者是子组件。

let inst = new React.Component(props, context);
inst.render = function(props, state, context) {
  return Foo(props, context);
};
复制代码

或许你能够说彻底能够不用 constructor 的也能实现啊,这就是 preact 的精妙之处了,在源码中会有一个数组队列 recyclerComponents,这是专门用来回收销毁组件的,它的判断依据也是利用 constructor 属性:

if (recyclerComponents[i].constructor === Foo) {
  // ...
}
复制代码

prototype 原型链

js 每一个对象都会拥有一个原型对象,即 prototype属性。

function Person() {}
复制代码

Person 对象的原型对象就是 Person.prototype 对象:

原型

Person.prototype 对象里有那些属性:

原型属性

能够看出这个对象默认拥有两个原生属性 constructor__proto__

constructor 上面说过了,全部的对象都会有,那么 __proto__ 也是全部的对象都会有,它是一个内建属性,经过它能够访问到对象内部的 [[Prototype]] ,它的值能够是一个对象,也能够是 null

那么 __proto__ 究竟是什么呢:

function Person() {}
let p1 = new Person();
复制代码

__proto__

图中的两个红框能够看出,p1.__proto__Person.prototype 指向了同一个对象。

// true
p1.__proto__ === Person.prototype;
复制代码

三者关系

Person 对象能够从这个原型对象上继承其方法和属性,因此 Person 对象的实例也能访问原型对象的属性和方法,可是这些属性和方法不会挂载在这个实例对象自己上,而是原型对象的构造器的原型 prototype 属性上。

那么,Person.prototype__proto__ 又指向哪里呢?

__proto__

看上图,能够看出 p1.__proto__.__proto__ 指向了 Object.prototypep1.__proto__.__proto__.__proto__ 最后指向了 null,由此能够看出了构建了一条原型链

原型链的构建依赖于实例对象的 __proto__ ,并非原对象的 prototype

ES6 设置获取原型的方法:

// 给p1原型设置属性
Object.setPrototypeOf(p1, { name: "zhangsan" });

// zhangsan
console.log(p1.name);

// {name: "zhangsan"}
Object.getPrototypeOf(p1);
复制代码

Object.setPrototypeOf

上图红框能够看出,Object.setPrototypeOf 其实就是新的语法糖,至关于给 P1.__proto__ 这个属性赋值。

一个简单的案例:

另外一个案例

经典的原型链图示:

原型链

举例,如何用原生 js 实现 Student 继承 Person

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

function Student(name, age) {
  this.name = name;
  this.age = age;
}
Student.prototype.getInfo = function() {
  return `${this.name} and ${this.age}`;
};
复制代码

实现继承,即要 Student 的实例可以访问 Person 的属性和方法,也要能访问 Person 原型上的方法 getName

首先来看下 es6 的继承:

class Person {
    public name: string
    constructor(name) {
        this.name = name
    }
    getName(){
        return this.name
    }
}

class Student extends Person {
    public age: number
    constructor(name, age) {
        super(name)
    }

    getAge() {
        return this.age
    }
}

let s = new Student('zhangsan', 20)

// zhangsan
s.name
// zhangsan
s.getName()
// 20
s.getAge()

复制代码

那么,用原生 js 怎么作呢,下面来一步一步的实现。

  • call 实现函数上下文继承
function Person(name) {
  this.name = name;
}
Person.prototype.getName = function() {
  return this.name;
};

function Student(name, age) {
  Person.call(this, name);
  this.age = age;
}
Student.prototype.getInfo = function() {
  return `${this.name} and ${this.age}`;
};

let s = Student("zhangsan", 20);

// zhangsan
s.name;

// error
s.getName();
复制代码

call 方法只是改变了 Person 中函数体内的 this 指向,并不能改变它的原型,因此没法访问 Person 方法的原型。

  • 原型链实现原型继承
function Person(name) {
  this.name = name;
}
Person.prototype.getName = function() {
  return this.name;
};

function Student(age) {
  this.age = age;
}
Student.prototype.getInfo = function() {
  return this.age;
};

Student.prototype = new Person("zhangsan");

let s = new Student(20);

// zhangsan
s.getName();
复制代码

Student.prototype 的值设置为父类的实例对象,这样就能很简单的实现 Student 的实例对象能访问到 Person 的原型,可是这也是也有问题的,与其说继承的是 Person 这个类,不如说是继承的是这个类的实例对象,就是 name = zhangsan 这个实例,和 oop 的思想有背。

  • 上面两种方式的结合,完美解决这个问题
/** * 继承函数的核心方法 */
function _extends(child, parent) {
  // 定义一个中间函数,并设置它的 constructor
  function __() {
    this.constructor = child;
  }
  // 这个函数的原型指向父类的原型
  __.prototype = parent.prototype;
  // 子类的原型窒息那个这个中间函数的实例对象
  child.prototype = new __();
}
复制代码

这个 _extends 方法,是实现的核心,两个知识点,一是定义了一个无参数的中间函数,并设置它的 constructor;第二个就是对原型链的使用。

function Person(name) {
  this.name = name;
  this.getName1 = function() {
    console.log("Person", this.name);
  };
}

Person.prototype.getName = function() {
  console.log("Person prototype", this.name);
};

// 这个方法必定要在定义子类原型以前调用
_extends(Student, Person);

function Student(name, age) {
  this.age = age;
  Person.call(this, name);
}
Student.prototype.getInfo = function() {
  console.log("Student", this.age);
};

let s = new Student("zhangsan", 12);

// Person prototype zhangsan
s.getName();
// Student 12
s.getInfo();
复制代码

这样,就能简单是实现了继承,而且多重继承也是支持的

// 多重继承
_extends(MidStudent, Student);
function MidStudent(name, age, className) {
  this.className = className;
  Student.call(this, name, age);
}

let mids = new MidStudent("lisi", 16, "class1");
// Person prototype lisi
mids.getName();
// Student 16
mids.getInfo();
复制代码

有兴趣能够多多研究研究,网上有很多精品案例,这个是 js 的基础,确定比成天调用 api 有意思的多,收获的也会更多。

最后说下 new 一个对象到底发生了什么

俗话说,没有女友的你能够new 一个对象,那么这个 new 一下,到底经历了什么呢。

let p1 = new Person();
复制代码

step1,让变量p1指向一个空对象

let p1 = {};
复制代码

step2, 让 p1 这个对象的 __proto__ 属性指向 Person 对象的原型对象

p1.__proto__ = Person.prototype;
复制代码

step3, 让 p1 来执行 Person 方法

Person.call(p1);
复制代码

如今看这个流程,是否是很简单,是否是有种豁然开朗的感受!

那要如何实现一个本身的 new 呢?

/** * Con 目标对象 * args 参数 */
function myNew(Con, ...args) {
  // 建立一个空的对象
  let obj = {};
  // 连接到原型,obj 能够访问到构造函数原型中的属性
  obj.__proto__ = Con.prototype;
  // 绑定 this 实现继承,obj 能够访问到构造函数中的属性
  let ret = Con.call(obj, ...args);
  // 优先返回构造函数返回的对象
  return ret instanceof Object ? ret : obj;
}
复制代码

来测试下:

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

Person.prototype.getName = function() {
  console.log(`your name is ${this.name}`);
};

let p2 = myNew(Person, "lisi");

// your name is lisi
p2.getName();
复制代码

完美实现了!

相关文章
相关标签/搜索