前端

彻底搞懂JavaScript原型

2020-06-12  本文已影响0人  zxhnext

开局先放一张图,接下来我们可以对照这张图来理解下面的内容。


原型链

1. 原型

1.1 引用类型

所有的引用类型(数组、对象、函数),都具有对象特性,即可自由扩展属性(除了"null"意外)

1.2 隐式原型

所有的引用类型(数组、对象、函数),都有一个proto(隐式原型) 属性,属性值是一个普通的对象

1.3 显式原型

所有的函数,都有一个prototype(显式原型)属性,属性值也是一个普通的对象。

2. 构造函数

来看下什么是构造函数。在 JavaScript 中,用 new 关键字来调用的函数,称为构造函数。来看下面一个例子:

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

var p1 = new Person('zs', '男', 'basketball');
var p2 = new Person('ls', '女', 'dancing');

2.1 构造函数执行过程

function Animal(color) {
  this.color = color;
}

当一个函数创建好以后,我们并不知道它是不是构造函数,即使像上面的例子一样,函数名为大写,我们也不能确定。只有当一个函数以 new 关键字来调用的时候,我们才能说它是一个构造函数。就像下面这样:

var dog = new Animal("black");

此时,构造函数会有以下几个执行过程:
(1) 当以 new 关键字调用时,会创建一个新的内存空间,标记为 Person 的实例。


创建内存空间

(2) 函数体内部的 this 指向该内存


函数体内部指向该内存
通过以上两步,我们就可以得出这样的结论。
var p2 = new Person('ls', '女', 'dancing');  // 创建一个新的内存 #f2
var p3 = new Person('ww', '女', 'singing');  // 创建一个新的内存 #f3

每当创建一个实例的时候,就会创建一个新的内存空间(#f2, #f3),创建 #f2 的时候,函数体内部的 this 指向 #f2, 创建 #f3 的时候,函数体内部的 this 指向 #f3。

(3) 执行函数体内的代码
通过上面的讲解,你就可以知道,给 this 添加属性,就相当于给实例添加属性。

(4) 默认返回 this 。
由于函数体内部的 this 指向新创建的内存空间,默认返回 this ,就相当于默认返回了该内存空间,也就是上图中的 #f1。此时,#f1的内存空间被变量 p1 所接受。也就是说 p1 这个变量,保存的内存地址就是 #f1,同时被标记为 Person 的实例。

2.2 构造函数的返回值

构造函数执行过程的最后一步是默认返回 this 。言外之意,构造函数的返回值还有其它情况。下面我们就来聊聊关于构造函数返回值的问题。

(1)没有手动添加返回值,默认返回 this 。

function Person1() {
  this.name = 'zhangsan';
}

var p1 = new Person1();

按照上面讲的,我们复习一遍。首先,当用 new 关键字调用时,产生一个新的内存空间 #f11,并标记为 Person1 的实例;接着,函数体内部的 this 指向该内存空间 #f11;执行函数体内部的代码;由于函数体内部的 this 指向该内存空间,而该内存空间又被变量 p1 所接收,所以 p1 中就会有一个 name 属性,属性值为 'zhangsan'。

p1: {
  name: 'zhangsan'
}

(2) 手动添加一个基本数据类型的返回值,最终还是返回 this。

function Person2() {
  this.age = 28;
  return 50;
}

var p2 = new Person2();
console.log(p2.age);   // 28
p2: {
  age: 28
}

如果上面是一个普通函数的调用,那么返回值就是 50。

(3) 手动添加一个复杂数据类型(对象)的返回值,最终返回该对象

function Person3() {
  this.height = '180';
  return ['a', 'b', 'c'];
}

var p3 = new Person3();
console.log(p3.height);  // undefined
console.log(p3.length);  // 3
console.log(p3[0]);      // 'a'
再来一个例子

function Person4() {
  this.gender = '男';
  return { gender: '中性' };
}

var p4 = new Person4();
console.log(p4.gender);  // '中性'

关于构造函数的返回值,无非就是以上几种情况。

以上构造函数参考: JS进阶(1) —— 人人都能懂的构造函数

2.3 实现一个new

创建一个自定义类
=>创建一个函数(Function类的实例),直接执行就是普通函数,但是“new 执行”它则被称为一个自定义的类

NEW 函数执行

  1. 形成一个全新的执行上下文EC
  2. 形成一个AO变量对象
  3. ARGUMENTS
  4. 形参赋值
  5. 初始化作用域链
  6. [新]默认创建一个对象,而这个对象就是当前类的实例
  7. [新]声明其THIS指向,让其指向这个新创建的实例
  8. 代码执行
  9. [新]不论其是否写RETURN,都会把新创建的实例返回(特殊点)
function func() {
    // let obj={}; //=>这个对象就是实例对象
    // this -> obj
    let x = 100;
    this.num = x + 100; //=>相当于给创建的实例对象新增一个num的属性 obj.num=200 (因为具备普通函数执行的一面,所以只有this.xxx=xxx才和创建的实例有关系,此案例中的x只是AO中的私有变量)
    // return obj;  用户自己返回内容,如果返回的是一个引用类型值,则会把默认返回的实例给覆盖掉(此时返回的值就不在是类的实例了)
}
let f = new func();
console.log(f); //=>f是func这个类的实例 {num:200}

let f2 = new func();
console.log(f === f2); //=>false 每一次new出来的都是一个新的实例对象(一个新的堆内存)

console.log(f instanceof func); //=>TRUE instanceof用来检测某一个实例是否属于这个类

func(); //=>this:window  AO(FUNC):{x=100} ... 普通函数执行
/* 
    * 内置NEW的实现原理 
    * @params
    *    Func:操作的那个类
    *    ARGS:NEW类的时候传递的实参集合
    * @return
    *    实例或者自己返回的对象
    */
function _new(Func, ...args) {
    //默认创建一个实例对象(而且是属于当前这个类的一个实例)
    // let obj = {};
    // obj.__proto__ = Func.prototype; //=>IE大部门浏览器中不允许我们直接操作__proto__
    let obj = Object.create(Func.prototype);

    //也会把类当做普通函数执行
    //执行的时候要保证函数中的this指向创建的实例
    let result = Func.call(obj, ...args);

    //若客户自己返回引用值,则以自己返回的为主,否则返回创建的实例
    if ((result !== null && typeof result === "object") || (typeof result === "function")) {
        return result;
    }
    return obj;
}
let sanmao = _new(Dog, '三毛');
sanmao.bark(); //=>"wangwang"
sanmao.sayName(); //=>"my name is 三毛"
console.log(sanmao instanceof Dog); //=>true

2.4 instanceof

instanceof运算符用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置。
●vara= {}其实是var a = new Object()的语法糖
●var a = []其实是var a = new Array()的语法糖
●function Foo(...}其实是var Foo = new Function(..)
●使用instanceof判断一个函数是否是一个变量的构造函数

instanceof判断一个变量是否是数组

var arr = []
arr instanceof Array // true
typeof arr // object, typeof 是无法判断是否是数组的

2.5

function Fn() {
    this.x = 100;
    this.y = 200;
    this.getX = function () {
        console.log(this.x);
    }
}
Fn.prototype.getX = function () {
    console.log(this.x);
};
Fn.prototype.getY = function () {
    console.log(this.y);
};
let f1 = new Fn;
let f2 = new Fn;
console.log(f1.getX === f2.getX);
console.log(f1.getY === f2.getY);
console.log(f1.__proto__.getY === Fn.prototype.getY);
console.log(f1.__proto__.getX === f2.getX);
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.constructor);
console.log(Fn.prototype.__proto__.constructor);
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();

3. 原型链解析

所有的引用类型(数组、对象、函数),proto属性值指向它的构造函数的"prototype"属性值。

当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的proto(即它的构造函数的prototype)中寻找。

原型链
console.log(obj._ proto__ === object. prototype)
var obj = {}; obj.a = 100;
var arr = []; arr.a = 100;
function fn () {}
fn.a = 100;

console. log(obj.__proto__);
console. log(arr.__proto__);
console. log(fn.__proto__);

console. log(fn. prototype)
console. log(obj.__proto__===Object. prototype)

3.1 小测试

function f1() {}
console.log(typeof f1.prototype)  // Object
console.log(typeof Function.prototype)  // Function
console.log(typeof Object.prototype)  // Object
console.log(typeof Function.prototype.prototype)  // Object
// 万物皆对象
Object.prototype.name = "zhijia";
1.0.name  // zhijia
1..name  // zhijia
1.name  // 报错 因为不知道 . 属于1还是属于name,所以报错
Function.name  // Function function的原型链与object不在一起,所以打出函数名字

3.2 原型链继承

var father = function(color) {
    // constructor == Car 构造函数和初始化这个类是一个东西, constructor即函数本身
    this.color = color
    console.log(111)
}
father.prototype.sail = function() {
    // 挂载到原型上new的时候可以少实例化一次,省去重新构建的成本
    console.log('这是'+this.color+'色')
}
var son = function(color) {
    // father.call(this) // 绑定this,不写color new son的时候传color时会获取不到,可自测
    father.call(this,color) // 绑定this
}
// son.prototype = father.prototype // 子类修改时会修改父类,因为这是按引用传递
// new的时候构造函数会执行
// 1. 拿到父类原型链上的方法
// 2. 不能让构造函数执行两次
// 3. 引用的原型链不能按址引用
// 4. 修正子类的constructor
var __pro = Object.create(father.prototype); // 获取副本
__pro.constructor = son; // 与下句交换位置无差别,自测
son.prototype = __pro;

这里我们顺便提一下es6的继承,相比于es5就简单的多了

// es6继承
class People{
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    getName(){
        return this.name;
    }
}
class English extends People{
    constructor(name,age,language){
        super(name,age)
        this.language = language;
    }
    introduce(){
        console.log("hi,I am" + this.getName())
        console.log("I speak" + this.language)
    }
} 

let en = new English('Byron',26,'english')
en.introduce();
上一篇下一篇

猜你喜欢

热点阅读