JavaScript 函数的特性与原型链讲解

4,542 阅读12分钟

文章更新时间:2024-08-06

开篇语

这次给大家带来一个关于 JavaScript 函数与原型链之间的关系的分享,将会从函数开始讲起,一直讲到整个原型链是什么样子的,希望能给大家带来帮助。

函数的三种使用方式

JavaScript 中的函数大致有以下三种使用方式:

一、作为普通函数来使用

例如

// 定义函数
function foo() {}
// 调用函数
foo();

二、作为构造函数来使用

当一个函数被用来创建新对象的时候,就把它叫做:构造函数。

例如

// 按照惯例,作为构造函数的函数名首字母需要大写
function Foo() {}
const obj = new Foo();

这时对于对象 obj 来说,函数 Foo 就叫做它的构造函数。

三、作为对象来使用

访问对象的属性可以使用点操作符和中括号来访问

例如

function foo() {}
foo.name = 'tom';
foo['age'] = 20;

以上这三种使用方式中,以普通函数来调用的方式大家都懂,这里不再赘述。 本文主要讲解的就是当一个函数被作为构造函数来使用和被作为对象来使用的时候,分别是什么样的,以及它们之间与原型链的关系是什么样的。

预留问题

在讲接下来的内容之前,我们先来看几个问题:

// 首先,定义一个函数,将会作为构造函数
function Foo() {}

// 实例化出来一个对象
const obj = new Foo();

// 在 Object 的原型上定义一个属性:objProp
Object.prototype.objProp = '我是 Object 原型上的属性';

// 在 Function 的原型上定义一个属性:funcProp
Function.prototype.funcProp = '我是 Function 原型上的属性';

// 你预想一下,以下这些分别会输出什么?
console.log(obj.objProp) // ?
console.log(obj.funcProp) // ?

console.log(Foo.objProp) // ?
console.log(Foo.funcProp) // ?

console.log(Object.objProp) // ?
console.log(Object.funcProp) // ?

console.log(Function.objProp) // ?
console.log(Function.funcProp) // ?

console.log(Array.objProp) // ?
console.log(Array.funcProp) // ?

作为构造函数来使用

接下来会讲解函数的第二个特性,作为构造函数来使用的时候,它是什么样的。

使用 new 操作符创建对象

当我们使用 new 操作符来从构造函数创建对象的时候,会经历以下几个步骤:

  1. 创建一个空对象:{}
  2. 把这个空对象的原型链 __proto__ 指向构造函数的原型对象。
  3. 把新对象作为上下文来调用构造函数,即改变(或者叫绑定)了构造函数里面的 this 指向,这样新对象就相当于被设置了构造函数里面使用 this.xxx 定义的属性了。
  4. 如果该函数没有返回对象,则返回这个新对象。

文字描述不太好理解,我们用一个示例代码来描述一遍,对比着看一下:

// 新建一个构造函数
function Foo() {
  this.boo = '内部属性';
}
// 使用 new 操作符实例化一个对象
const obj_foo = new Foo()
// ========创建对象的过程:
// 1. 创建一个空对象:`{}`。
const obj = {};
// 2. 原型链的链接过程,把这个空对象的原型链 __proto__ 指向构造函数的原型对象。
// 这样 obj 就能访问到 Foo 上的属性了。
obj.__proto__ = Foo.prototype;
// 3. 把新对象绑定为 Foo 的 this,这样 Foo 内部的 this 就被绑定为 obj 了。
// 经过这一步骤后,obj 就相当于被设置上了 boo 这个属性
Foo.apply(obj, arguments);
// 4. 因为 Foo 没有返回对象,所以就返回这个新对象
return obj;
// ========END
// 这时 obj_foo 就是返回出来的新对象了
// 就能访问:obj_foo.boo 了。

__proto__ 是什么?

首先你要记住:只有对象才会有 __proro__ 这个属性。它是 js 得以实现原型链的关键,因为它会指向构造函数的原型对象,即 prototype 属性上的对象,即:对象.__proto__ === 构造函数.prototype。而构造函数原型对象上的 __proto__ 又指向上一级构造函数的原型对象,即:构造函数.prototype.__proto__ -> 上一级构造函数.prototype,以此类推层层往上,于是就形成了我们所说的原型链。

prototype 是什么?

前面一直提到 构造函数.prototype,这个属性是构造函数特有的一个属性,它是构造函数的原型对象,所有由它构造出来的对象都会从这个原型对象上继承方法和属性(参考 MDN:JavaScript 是一种基于原型的语言,每个对象都拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性)。就像前面的例子:const obj_foo = new Foo()obj_foo 的原型对象就是构造它的函数 Foo 的 prototype,obj_fooFoo.prototype 上继承了方法和属性。

又因为 构造函数.prototype 是一个对象,所以它就会有 __proto__ 这个属性,说明原型对象也是由一个构造函数创建出来的,即:构造函数.prototype.__proto__ -> 上一级构造函数.prototype

这下你能理解 __proto__、prototype 和原型链之间的关系了吧,正因为有了这两个属性,JavaScript 才得以实现原型链。至于 上一级构造函数 是什么?下文将会继续讲解。


特殊的构造函数

先来回顾一下 JavaScript 中的一些内建构造函数吧,比如:ObjectFunctionNumberArrayString等。

根据前文的知识可以推理出:既然是构造函数,就可以用来创建对象。

比如,用 Number 构造函数创建一个数字对象,用 String 构造函数创建一个字符串对象:

var numObj = new Number('123'); // 判断 numObj === 123,得到 false,说明 numObj 不是数字
// 因为 Number.prototype 上有 toString() 方法,于是 numObj 也就继承了该方法:
var numStr = numObj.toString(); // 判断 numStr === '123' 得到 true
var strObj = new String(123); // 判断 strObj === '123' 得到 false,说明 strObj 不是字符串
// 判断 numObj instanceof Number 得到 true
// 判断 strObj instanceof String; // 得到 true

以上例子可以说明构造函数是可以用来创建对象的。

有了上述印象,接下来就跟大家讲解其中最核心最特殊的两个构造函数:ObjectFunction

Object 构造函数

Object 构造函数可以用来创建对象。

比如:

const obj = new Object();

Object 除了是一个构造函数,可以用来创建对象外,它还拥有一个很硬核的能力:派生所有的其它构造函数

这里用了“派生”二字,在理解上是把它当做一个基类或者说父类来看待的。

也就是:所有的其它构造函数都是由 Object 这个基类派生出来的(子类),或者说其它构造函数都继承自 Object

用代码解释一下(注:JavaScript 中没有明确的类的概念):

// 父类 Object
class Object {}
// 子类 MyFunction
class MyFunction extends Object {}

正因为构造函数是由 Object 构造出来的,那么构造函数的原型链就指向了 Object 的原型对象(参考 new 的第二步过程可以加以理解):

构造函数.prototype.__proto__ === Object.prototype

重点

因为 Function 也是一个构造函数,那么就可以推导出:

Function.prototype.__proto__ === Object.prototype

Function 构造函数

Function 构造函数是用来创建函数对象的,例如:

const sum = new Function('a', 'b', 'return a + b');
sum instanceof Function; // true

虽然 sum 是一个函数,但在这里把 sum 叫函数对象,是因为它确实是由 Function 构造(new)出来的一个对象,而且它跟我们平常定义一个 sum 函数的效果是一样的,即:

// 上面的定义跟这样的定义,效果是一样的
function sum(a, b) {return a + b}

这就是我当初的迷惑点所在。即函数有多种表示形态,可以是普通函数,可以是构造函数,同样它也可以是函数对象。

于是我们不难得出:不论是通过什么样的定义得到的函数,函数对象都是由 Function 构造出来的

证明:sum.__proto__ === Function.prototype 返回 true

重点

既然这样,那我们再来看 Object 这个特殊的函数对象。它既然可以作为对象,那么我们就可以推导出:

Object.__proto__ === Function.prototype 返回 true,看到没,如果把 Object 当做一个函数对象来使用,它就是由 Function 这个构造函数构造出来的。

再对比一下上面的 Object 这个基类派生构造函数的等式:Function.prototype.__proto__ === Object.prototype

是不是觉得蛮有意思的,把函数作为不同的形态来看待,它就会有不同的原型链上的表现。

Object 构造函数是由谁构造出来的

那么问题来了,Object 构造函数又是由谁构造出来的呢?

即:Object.protoType.__proto__ 指向谁?因为所有构造函数都是由 Object 派生出来的了,那它自己又是由谁生出来的嘞?

答案是:语言本身。

因为 Object.protoType.__proto__ === null,到这里就走到原型链的尽头了,由语言本身创建出来了这个 Object 构造函数。

阶段小结

  1. 函数有三种形态:普通函数,构造函数和函数对象;
  2. 当使用 new 操作符调用构造函数时,会创建一个新对象出来;
  3. 只有对象才有 __proto__ 这个属性,它指向对象的构造函数的原型对象,对象.__proto__ === 构造函数.prototype
  4. 只有构造函数才有 prototype 这个属性,它是该构造函数的原型对象;
  5. 所有的构造函数都是由 Object 构造(派生)出来的,构造函数.prototype.__proto__ === Object.prototype
  6. 所有的函数对象都是由 Function 创建出来的,函数对象.__proto__ === Function.prototype

函数是如何确定形态的

前面我们讲了构造函数、函数对象这些东西,那么函数是如何确定最终形态的呢?答案是:在使用的时候。

使用 new 来调用它,那么它就是一个可以创建对象的构造函数。如果使用点或者中括号来调用它,那么它就又变成了一个函数对象。如果访问它的 prototype.__proto__ 那么就是要去看它的父类是谁。

我在前面标了几个重点公式:

// 1
Function.prototype.__proto__ === Object.prototype
// 2
Object.prototype.__proto__ === null
// 3
Function.__proto__ === Function.prototype
// 4
Object.__proto__ === Function.prototype

再次解释一下:

1、看到 Function.prototype 就说明 Function 此时是被当做构造函数来使用的,前面说过所有构造函数都是由 Object 构造(派生)出来的。

2、看到 Object.prototype 就说明 Object 此时是被当做构造函数来使用的,前面说过所有构造函数都是由 Object 构造(派生)出来的,又因为它不能自己生自己,所以它只能是由语言本身创建出来。

3、看到 Function.__proto__ 就说明 Function 此时是被当做函数对象来使用的,前面说过所有函数对象都是由 Function 创建出来的。

4、看到 Object.__proto__ 就说明 Object 此时是被当做函数对象来使用的,前面说过所有函数对象都是由 Function 创建出来的。

所以不同的使用方式,需要用不同的知识点来去做区分。

解决预留问题

这下我们就可以来解决预留问题了。

// 首先,定义一个函数,将会作为构造函数
function Foo() {}

// 实例化出来一个对象
const obj = new Foo();

// 在 Object 的原型上定义一个属性:objProp
Object.prototype.objProp = '我是 Object 原型上的属性';

// 在 Function 的原型上定义一个属性:funcProp
Function.prototype.funcProp = '我是 Function 原型上的属性';

问题:

1、

console.log(obj.objProp) // ?

答案:

  1. 因为对象的原型链指向构造函数的原型对象,所以:obj.__proto__ -> Foo.prototype,因为 Foo.prototype 上没有 objProp 这个属性,所以会顺着原型链继续往上找;
  2. 因为所有构造函数都是由 Object 派生出来的,所以会顺着这样的原型链进行查找:Foo.prototype.__proto__ -> Object.prototype
  3. 此时在 Object.prototype 原型上找到了 objProp 这个属性,所以 obj.objProp 返回数据。

2、

console.log(obj.funcProp) // ?

答案:

  1. 因为对象的原型链指向构造函数的原型对象,所以:obj.__proto__ -> Foo.prototype,因为 Foo.prototype 上没有 objProp 这个属性,所以会顺着原型链继续往上找;
  2. 因为所有构造函数都是由 Object 派生出来的,所以会顺着这样的原型链进行查找:Foo.prototype.__proto__ -> Object.prototype
  3. 由于在 Object.prototype 上找不到 funcProp 属性,会根据这样的原型链继续找:Object.prototype.__proto__ -> null
  4. 最终找不到,返回 undefined。

3、

console.log(Foo.objProp) // ?

答案:

  1. 看到 Foo.objProp 就说明是把 Foo 当做对象来使用的,此时它是一个函数对象,所以它是由 Function 创建出来的,于是按照这样的原型链来查找:Foo.__proto__ -> Function.prototype
  2. 由于在 Function.prototype 上找不到 objProp 属性,根据这样的原型链继续找:Function.prototype.__proto__ -> Object.prototype
  3. Object.prototype 上找到了 objProp 属性,所以 Foo.objProp 返回数据。

4、

console.log(Foo.funcProp) // ?

答案:

  1. 看到 Foo.objProp 就说明是把 Foo 当做对象来使用的,此时它是一个函数对象,所以它是由 Function 创建出来的,于是按照这样的原型链来查找:Foo.__proto__ -> Function.prototype
  2. Function.prototype 上找到了 funcProp 属性,所以 Foo.funcProp 返回数据。

5、

console.log(Object.objProp) // ?

答案:

  1. 看到 Object.objProp 就说明是把 Object 当做对象来使用的,此时它是一个函数对象,所以它是由 Function 创建出来的,于是按照这样的原型链来查找:Object.__proto__ -> Function.prototype
  2. 由于在 Function.prototype 上找不到 objProp 属性,于是根据这样的原型链继续找:Function.prototype.__proto__ -> Object.prototype
  3. Object.prototype 上找到了 objProp 属性,所以 Object.objProp 返回数据。

6、

console.log(Object.funcProp) // ?

答案:

  1. 看到 Object.objProp 就说明是把 Object 当做对象来使用的,此时它是一个函数对象,所以它是由 Function 创建出来的,于是按照这样的原型链来查找:Object.__proto__ -> Function.prototype
  2. Function.prototype 上找到了 funcProp 属性,所以 Object.funcProp 返回数据。

7、

console.log(Function.objProp) // ?

注:为了避免混淆,Function 表示对象, CFunction 表示构造函数

答案:

  1. 看到 Function.objProp 就说明是把 Function 当做对象来使用的,此时它是一个函数对象,所以它是由 CFunction 创建出来的,于是按照这样的原型链来查找:Function.__proto__ -> CFunction.prototype
  2. CFunction.prototype 上找不到 objProp 属性,根据这样的原型链继续找:CFunction.prototype.__proto__ -> Object.prototype
  3. Object.prototype 上找到了 objProp 属性,所以 Function.objProp 返回数据

8、

console.log(Function.funcProp) // ?

答案:

  1. 看到 Function.objProp 就说明是把 Function 当做对象来使用的,此时它是一个函数对象,所以它是由 CFunction 创建出来的,于是按照这样的原型链来查找:Function.__proto__ -> CFunction.prototype
  2. CFunction.prototype 上找到了 funcProp 属性,所以 Function.funcProp 返回数据

9、

console.log(Array.objProp) // ?

答案:自己尝试着分析一下吧,欢迎在评论区给出

10、

console.log(Array.funcProp) // ?

答案:自己尝试着分析一下吧,欢迎在评论区给出

原型链图片

我画了一张原型链的图片,供大家对比着上述的流程作参考:

如果本文有什么错误之处,还请大家帮忙指出,我会继续改进。


更新记录

12-05:重新组织了内容结构,修复文字错误,优化原型链图片。 24-07-22:优化描述不清晰和难懂的地方,让知识点更易于理解。