JavaScript中this、constructor与prototype机制深度解析

作者:木棉时间:2026-10-30 05:14:53浏览:140

本文深入解析JavaScript核心机制:this的动态绑定规则、prototype原型对象的作用及扩展陷阱,以及constructor属性在原型覆盖时的行为变化。通过具体代码示例,阐明apply/call对this的影响、for-in循环枚举原型方法的问题及修复方案,帮助开发者掌握对象创建与继承的关键细节。

前言

在JavaScript开发中,this、constructor和prototype是三个基础且核心的概念。深入理解它们的含义和工作机制对于编写高质量代码至关重要。本文旨在重新梳理这些知识点,通过温故知新的方式,帮助读者巩固基础。

this关键字的动态绑定与指向控制

this关键字

this关键字代表当前执行上下文中的对象。当在全局作用域中使用时,this指向当前页面的window对象;而在函数内部使用时,this的具体指向取决于函数被调用时的上下文环境,即函数是在哪个对象上被调用的。此外,开发者可以利用全局方法apply和call来显式地改变函数内部this的指向。

首先,我们来看一个在全局作用域中使用this的示例:


console.log(this === window); // true
console.log(window.alert === this.alert); // true
console.log(this.parseInt("021", 10)); // 10

需要注意的是,函数内部的this是在运行时动态决定的,而非在函数定义时确定。具体表现如下:


// 定义一个全局函数
    function foo() {
      console.log(this.fruit);
    }
    // 定义一个全局变量,等价于window.fruit = "apple";
    var fruit = "apple";
    // 此时函数foo中this指向window对象
    // 这种调用方式和window.foo();是完全等价的
    foo(); // "apple"

    // 自定义一个对象,并将此对象的属性foo指向全局函数foo
    var pack = {
      fruit: "orange",
      foo: foo
    };
    // 此时函数foo中this指向window.pack对象
    pack.foo(); // "orange"

JavaScript提供了全局方法apply和call,它们的功能相同,主要区别在于参数传递的方式不同。这两个方法可用于改变函数执行时this的指向,示例如下:


// 定义一个全局函数
    function foo() {
      console.log(this.fruit);
    }

    // 定义一个全局变量
    var fruit = "apple";
    // 自定义一个对象
    var pack = {
      fruit: "orange"
    };

    // 等价于window.foo();
    foo.apply(window); // "apple"
    // 此时foo中的this === pack
    foo.apply(pack);  // "orange"

注:apply和call两个函数的作用相同,唯一的区别是两个函数的参数定义不同。

由于在JavaScript中函数本身也是对象,因此我们可以观察到如下有趣的代码行为:


// 定义一个全局函数
    function foo() {
      if (this === window) {
        console.log("this is window.");
      }
    }

    // 函数foo也是对象,所以可以定义foo的属性boo为一个函数
    foo.boo = function() {
      if (this === foo) {
        console.log("this is foo.");
      } else if (this === window) {
        console.log("this is window.");
      }
    };
    // 等价于window.foo();
    foo(); // this is window.

    // 可以看到函数中this的指向调用函数的对象
    foo.boo(); // this is foo.

    // 使用apply改变函数中this的指向
    foo.boo.apply(window); // this is window.

prototype原型对象的作用与扩展陷阱

prototype原型对象

prototype本质上是一个JavaScript对象。

每个函数都拥有一个默认的prototype属性。如果该函数被用于创建自定义对象,我们通常称其为构造函数。以下是一个简单的应用场景:


// 构造函数
    function Person(name) {
      this.name = name;
    }
    // 定义Person的原型,原型中的属性可以被自定义对象引用
    Person.prototype = {
      getName: function() {
        return this.name;
      }
    }
    var hao= new Person("haorooms");
    console.log(hao.getName());  // "haorooms"

作为类比,我们可以参考JavaScript中的内置数据类型,如字符串(String)、数字(Number)、数组(Array)、对象(Object)和日期(Date)等。

我们有理由推测,在JavaScript引擎内部,这些数据类型是通过构造函数实现的,例如:


// 定义数组的构造函数,作为JavaScript的一种预定义类型
    function Array() {
      // ...
    }

    // 初始化数组的实例
    var arr1 = new Array(1, 56, 34, 12);
    // 但是,我们更倾向于如下的语法定义:
    var arr2 = [1, 56, 34, 12];

同时,数组操作的许多内置方法(如concat、join、push等)也是在prototype属性中定义的。实际上,JavaScript所有固有数据类型都具有只读的prototype属性(这是合理的:如果修改了这些类型的prototype属性,预定义的方法将会丢失),但我们可以向其中添加自定义的扩展方法。


// 向JavaScript固有类型Array扩展一个获取最小值的方法
    Array.prototype.min = function() {
      var min = this[0];
      for (var i = 1; i < this.length; i++) {
        if (this[i] < min) {
          min = this[i];
        }
      }
      return min;
    };

    // 在任意Array的实例上调用min方法
    console.log([1, 56, 34, 12].min()); // 1

注意:这里存在一个陷阱,当向Array的原型中添加扩展方法后,使用for-in循环遍历数组时,这些扩展方法也会被枚举出来。以下代码说明了这一现象(假设已向Array原型中扩展了min方法):


var arr = [1, 56, 34, 12];
    var total = 0;
    for (var i in arr) {
      total += parseInt(arr[i], 10);
    }
    console.log(total);  // NaN

解决该问题的方法非常简单:


var arr = [1, 56, 34, 12];
    var total = 0;
    for (var i in arr) {
      if (arr.hasOwnProperty(i)) {
        total += parseInt(arr[i], 10);
      }
    }
    console.log(total);  // 103

constructor属性与原型覆盖行为解析

constructor构造函数属性

constructor属性始终指向创建当前对象的构造函数。例如:


// 等价于 var foo = new Array(1, 56, 34, 12);
    var arr = [1, 56, 34, 12];
    console.log(arr.constructor === Array); // true
    // 等价于 var foo = new Function();
    var Foo = function() { };
    console.log(Foo.constructor === Function); // true
    // 由构造函数实例化一个obj对象
    var obj = new Foo();
    console.log(obj.constructor === Foo); // true

    // 将上面两段代码合起来,就得到下面的结论
    console.log(obj.constructor.constructor === Function); // true

然而,当constructor遇到prototype时,会出现一些有趣的现象。我们知道每个函数都有一个默认的prototype属性,而该prototype的constructor默认指向这个函数本身,如下例所示:


function Person(name) {
      this.name = name;
    };
    Person.prototype.getName = function() {
      return this.name;
    };
    var p = new Person("haorooms");

    console.log(p.constructor === Person); // true
    console.log(Person.prototype.constructor === Person); // true
    // 将上两行代码合并就得到如下结果
    console.log(p.constructor.prototype.constructor === Person); // true

但是,当我们重新定义函数的prototype时(注意:此处是覆盖而非修改,与上例不同),constructor的行为会变得有些特殊,示例如下:


function Person(name) {
      this.name = name;
    };
    Person.prototype = {
      getName: function() {
        return this.name;
      }
    };
    var p = new Person("haorooms");
    console.log(p.constructor === Person); // false
    console.log(Person.prototype.constructor === Person); // false
    console.log(p.constructor.prototype.constructor === Person); // false

造成这种现象的原因是,覆盖Person.prototype时,等价于执行以下代码操作:


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

由于constructor始终指向创建自身的构造函数,此时Person.prototype.constructor === Object,即:


function Person(name) {
      this.name = name;
    };
    Person.prototype = {
      getName: function() {
        return this.name;
      }
    };
    var p = new Person("haorooms");
    console.log(p.constructor === Object); // true
    console.log(Person.prototype.constructor === Object); // true
    console.log(p.constructor.prototype.constructor === Object); // true

如何修正这一问题?方法也很简单,只需重新覆盖Person.prototype.constructor即可:


function Person(name) {
      this.name = name;
    };
    Person.prototype = {
      getName: function() {
        return this.name;
      }
    };
    Person.prototype.constructor = Person;
    var p = new Person("haorooms");
    console.log(p.constructor === Person); // true
    console.log(Person.prototype.constructor === Person); // true
    console.log(p.constructor.prototype.constructor === Person); // true

也可以采用如下写法:


function Person(name) {
      this.name = name;
    };
    Person.prototype = {
     constructor:Person,//指定constructor
      getName: function() {
        return this.name;
      }
    };

以上关于JavaScript中this、constructor和prototype的全面解析到此结束,希望能为读者提供参考。欢迎大家支持本站。

作者最新文章