原型链

67 阅读8分钟

一. 简单理解 函数即对象,对象即函数

JS里最基础就是 函数即对象,对象即函数 我们可以通过如下方式简单理解:

假如我们有这么一个元素结构(Item):

元素结构(Item):
    可执行代码区  // 该区代码可直接按顺序执行
    属性代码区  //  该区代码相当于对象的属性, 每一个属性可以指向任意的元素结构(Item)

该元素结构(Item)有如下的规则:

1.如果想执行它的"可执行代码区", 直接把它当函数调用即可, 比如像这样 Item()

2.如果想访问它的属性, 直接把它当对象一样使用即可, 比如像这样 Item.属性名

元素结构(Item):
    可执行代码区  // 通过 Item() 执行
    属性代码区  //  通过 Item.属性名 访问

接下来我们再增加一条规则:

3."可执行代码区"是可选的, 可以有也可以没有, 当有时我们称之为函数(function), 当没有时我们称之为对象(object)

元素结构(Item):
    可执行代码区  // 有时称为函数(function), 没有时称为对象(object)
    属性代码区  //  每一个属性可以指向任意的元素结构, 并根据指向的元素结构类型, 称呼该属性类型

好了, 在有了上面的理解基础, 我们再来看JS的函数和对象, 我们可以认为JS的所有函数和对象都是上面的结构, 差别就是函数有"可执行代码区", 对象没有, 它们的定义如下:

function func() {...}  // 定义了一个有"可执行代码区"的元素结构, 称为(typeof)函数(function)
console.log(typeof func) // 输出为function
func(); // 执行func中的"可执行代码"区
func.x = 'x'; // 访问func中的属性

var ob = {}  // 定义了一个没有"可执行代码区"的元素结构, 称为(typeof)对象(object)
console.log(typeof ob) // 输出为object
ob.y = 'y'; // 访问ob中的属性
ob(); // 错误, ob没有"可执行代码区", 看报错信息: TypeError: ob is not a function

函数就是有"可执行代码区"的元素结构(Item), 对象是没有"可执行代码区"的元素结构(Item) , 请大家记住这个理解, 我们后续将使用函数, 对象, 元素结构(Item) 来阐述后面的原理.

二.创建对象和原型链

下面我们从一个函数创建来详细讲解其中的原理

一. 基础理解

// 1
var X = function() { // 或者写成 function X()
    this.x = 'x';
    return 'HELLO';
}

通过如上语句我们创建了一个函数X, 然而在JS引擎内部不仅仅只是创建了一个函数, 还做了其它事情, 这些事情我们可以用代码表示如下:

// 2
var X = function() { // 或者写成 function X()
    this.x = 'x';
    return 'HELLO';
}

// 以下是JS引擎内部的隐式操作
let xPrototype = {}
X.prototype = xPrototype;
xPrototype.constructor = X;

上面代码 1 和 代码 2 我们可以认为是等价的, 代码 2 中多出来的部分, 可以认为是JS引擎自动操作的部分, 也就是说我们在创建函数的同时也创建了这个函数所关联的对象(它的prototype,称之为原型对象).

创建一个函数的同时为什么要给它创建一个原型对象呢, 这是为了后续对象创建和继承使用, 我们先记住:

函数拥有一个原型对象(prototype), 原型对象的属性constructor指向函数本身.

二.创建对象,原型链和类

每一个函数都可以通过new来创建新的对象,

// 1
var X = function() { // 或者写成 function X()
    this.x = 'x';
    return 'HELLO';
}

var xobj = new X(); // 通过new关键字来创建对象
var result = X(); // 此处是调用方法, 得到的是方法的返回值(return语句), 注意根new得到的是不一样的

函数(X)通过new创建的对象(xobj), 相当于函数内部的this(即this=xobj), 对this的所有操作都是操作在这个创建的对象上.

同时new创建的对象(xobj)继承函数的原型对象(X.prototype)的所有属性元素(无论属性是对象还是方法), 这也是函数要有原型对象的原因.

// 2
var X = function() { // 或者写成 function X()
    this.x = 'x';
    return 'HELLO';
}
X.prototype.sayHello = function() {console.log('hello');} 
X.sayHi = function() {console.log('hi');}

var xobj = new X();

console.log(xobj.x)  // this中指定的x
xobj.sayHello()  // 继承原型对象中的属性sayHello方法
xobj.sayHi() // 错误,new创建的对象只能继承函数原型对象中的属性元素,不继承函数本身的属性元素

为什么new创建的对象能继承函数原型对象的所有属性元素呢, 原来是因为JS引入了原型链(proto)

// 3
var X = function() { // 或者写成 function X()
    this.x = 'x';
}
X.prototype.sayHello = function() {console.log('hello');}

var xobj = new X();
console.log(xobj);  // 输出为 {x:'x'}
console.log(xobj.__proto__ == X.prototype); // 输出为true,  即xobj的原型链__proto__指向函数的原型对象prototype, 
// 当我们访问xobj的某一个属性时首先在xobj中查找, 如果找不到就会到__proto__指向的对象中查找

函数,原型对象和new出来的对象呈现如下关系:

函数通过new创建得到的对象有一个__proto__属性指向该函数的原型对象prototype, 即new X().proto == X.prototype, 且在访问对象的属性时优先找对象自身的属性, 如果没有则会向其__proto__指向的对象中去找, 同时如果__proto__所指对象中没有找到则会再向上一层__proto__.__proto__所指对象中去找, 直到找到为止, 这种由__proto__组成的对象链就是原型链.

争对如上情况, 在新的ES6中引入了类(class)的概念, 如上写法可以用es6表示为:

class X {
    constructor() {
        this.x = 'x';
        return 'HELLO';    
    }
    sayHello() {console.log('hello');}
    static sayHi() {console.log('hi');}
}

var xobj = new X()  // 该对象继承的属性元素与上面的一致

类(class)本质就是函数, 通过new关键字可以创建一个新的对象, 该新对象继承(__proto__指向)函数的原型对象(prototype)的所有属性元素(包括对象和方法), 且可在函数本身和原型对象的方法属性中通过this关键字访问该新对象

二.类的继承

类(class)的引入带来了诸多好处, 我们可以基于一个类创建很多新对象, 而这些新对象既有共同的结构又各自独立隔离, 这完全用上了面向对象的编程思想, 如果您学过JAVA就更能理解什么是面向对象.

然而在面向对象里, 光有类还不行, 还需要有类的继承, 这样才能更好的支撑面向对象的特性.

比如: 我们定义了一个类叫动物(Animal), 然后我们又定义了两个类叫狗(Dog)和猪(Pig), 无疑Dog和Pig拥有Animal的所有属性元素(包括对象和方法), 但类如果没有继承的特性, 我们就得将Animal的所有属性元素复制到Dog和Pig中, 如果还有其他很多具体的动物并且我们要修改Animal中的某个逻辑, 你就会发现你需要修改所有的具体动物的同一个逻辑, 这在编程中是"灾难"级的.

我们先用ES6的语法写一个类的继承:

class X {  // 定义类 X
    constructor() {
        this.x = 'x';
    }
    sayX() {
        console.log('say x');
    }
    static staticX() {
        console.log('static x');
    }
}


class Y extends X { // 定义类 Y 继承 X
    constructor() {
        super();
        this.y = 'y';
    }
    sayY() {
        console.log('say y');
    }
    static staticY() {
        console.log('static y');
    }
}

如上写法等价于非ES6语法的写法:

function X() {this.x = 'x';}
X.prototype.sayX = function(){console.log('say x');}
X.staticX = function(){console.log('static x');}


function Y() {
    X.call(this, arguments); // 注意此处要调用X, 相当于ES6中的super()
    this.y = 'y';
}
Y.prototype.sayY = function(){console.log('say y');}
Y.staticY = function(){console.log('static y');}

Y.prototype.__proto__ = X.prototype; // 注意此处建立原型链
Y.staticX = X.staticX; // 注意此处还要加上静态方法

通过如上继承, 我们使用类Y创建的对象将同时具有类Y和类X中定义的属性元素

var y = new Y()
y.sayY()  // 通过y.__proto__指向的Y.prototype对象中找到
y.sayX()  // 过过y.__proto__.__proto__指向的X.prototype对象中找到

y.staticX() // 错误, new创建的对象不继承类(函数)的属性, 只能是类(函数)的原型对象上的属性

同过如上建立各元素关系图如下:

其中最顶层的类Object是默认被继承的

通过如上关系图总结如下:

1.JS中万物皆对象(元素Item), 函数function是有可执行代码区的元素, 对象object是没有可执行代码区的元素

2.所有的函数都有一个原型对象prototype, 同时该原型对象的construtor属性指向该函数

3.所有函数同时可以看作一个类class, 通过new关键字可以依照该类创建新的对象, 该新对象的原型链(proto)指向该类的原型对象, 即 new X().proto == X.prototype

4.类在继承时相当于把该类的原型对象的原型链指向了父类的原型对象, 即Y.prototype.proto == X.prototype (Y是子类, X是父类)

5.默认Object是所有类的顶层父类, 即Y.prototype.proto == X.prototype (Y是子类, X是父类), X.prototype.proto == Object.prototype (默认)

6.访问查找对象属性(无论是对象还是方法)时, 会依据原型链(proto)一层层往上找, 直到找到为止.

7.对象(obj)原型链中对象的方法属性中的this关键字指向的就是对象(obj)本身

8.在使用ES6语法extends时, 除了原型链的建立还有类属性的拷备, 即 Y.staticX = X.staticX (Y是子类, X是父类)

转载自:JavaScript/Js 对象, 方法, 原型链, 继承底层原理简单理解 - Bovine的文章 - 知乎 zhuanlan.zhihu.com/p/523524805