【Web】JavaScript中的this陷阱(一)

本文详细解析了JavaScript中this关键字的工作原理及使用场景,包括全局作用域、函数内部、构造函数及原型链中的表现。

 今天刚看了一篇干货满满的博文,想把它整理一下搬运过来,并将内容中重点部分加以标注。限于原博篇幅很长,所以将分三篇进行整理。

JavaScript来自一门健全的语言,所以你可能觉得JavaScript中的this和其他面向对象的语言如java的this一样,是指存储在实例属性中的值。事实并非如此,在JavaScript中,最好把this当成哈利波特中的博格特的背包,有着深不可测的魔力。
  

JavaScript中很多时候会用到this,下面详细介绍每一种情况。在这里我想首先介绍一下宿主环境这个概念。一门语言在运行的时候,需要一个环境,叫做宿主环境。对于JavaScript,宿主环境最常见的是web浏览器, 浏览器提供了一个JavaScript运行的环境,这个环境里面,需要提供一些接口,好让JavaScript引擎能够和宿主环境对接。JavaScript引擎才是真正执行JavaScript代码的地方。常见的引擎有V8(目前最快JavaScript引擎、Google生产)、JavaScript core。JavaScript引擎主要做了下面几件事情:
- 一套与宿主环境相联系的规则;
- JavaScript引擎内核(基本语法规范、逻辑、命令和算法);
- 一组内置对象和API;
- 其他约定。
但是环境不是唯一的,也就是JavaScript不仅仅能够在浏览器里面跑,也能在其他提供了宿主环境的程序里面跑,最常见的就是nodejs。同样作为一个宿主环境,nodejs也有自己的JavaScript引擎–V8。根据官方的定义:
Node.js is a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications

1.global this


在浏览器里,在全局范围内,this等价于window对象。

 <script type="text/javascript">
     console.log(this === window); //true
 </script>

在浏览器里,在全局范围内,用var声明一个变量和给this或者window添加属性是等价的。

<script type="text/javascript">
     var foo = "bar";
     console.log(this.foo); //logs "bar"
     console.log(window.foo); //logs "bar"
 </script>

如果你在声明一个变量的时候没有使用var或者let(ECMAScript 6),你就是在给全局的this添加或者改变属性值。

<script type="text/javascript">
     foo = "bar"; 
     function testThis() {
            foo = "foo";
      } 
      console.log(this.foo); //logs "bar"
      testThis();
     console.log(this.foo); //logs "foo"
 </script>

在node环境里,如果使用REPL(Read-Eval-Print Loop,简称REPL:读取-求值-输出,是一个简单的,交互式的编程环境)来执行程序,this并不是最高级的命名空间,最高级的是global.

this
{ ArrayBuffer: [Function: ArrayBuffer],
  Int8Array: { [Function: Int8Array] BYTES_PER_ELEMENT: 1 },
  Uint8Array: { [Function: Uint8Array] BYTES_PER_ELEMENT: 1 },
  ...

> global === this
true

总结起来就是:在浏览器里面this是老大,它等价于window对象,如果你声明一些全局变量(不管在任何地方),这些变量都会作为this的属性。在node里面,有两种执行JavaScript代码的方式,一种是直接执行写好的JavaScript文件,另外一种是直接在里面执行一行行代码。对于直接运行一行行JavaScript代码的方式,global才是老大,this和它是等价的。在这种情况下,和浏览器比较相似,也就是声明一些全局变量会自动添加给老大global,顺带也会添加给this。但是在node里面直接脚本文件就不一样了,你声明的全局变量不会自动添加到this,但是会添加到global对象。所以相同点是,在全局范围内,全局变量终究是属于老大的。

2. function this


无论是在浏览器环境还是node环境, 除了在DOM事件处理程序里或者给出了thisArg(接下来会讲到)外,如果不是用new调用,在函数里面使用this都是指代全局范围的this。

<script type="text/javascript">
      foo = "bar";

      function testThis() {
        this.foo = "foo";
     }

      console.log(this.foo); //logs "bar"
      testThis();
     console.log(this.foo); //logs "foo"
</script>
test.js

foo = "bar";

function testThis () {
  this.foo = "foo";
}

console.log(global.foo);
testThis();
console.log(global.foo);
$ node test.js
bar
foo

除非你使用严格模式,这时候this就会变成undefined。

 <script type="text/javascript">
      foo = "bar";

      function testThis() {
        "use strict";
        this.foo = "foo";
      }

     console.log(this.foo); //logs "bar"
     testThis();  //Uncaught TypeError: Cannot set property 'foo' of undefined 
 </script>

如果你在调用函数的时候在前面使用了new,this就会变成一个新的值,和global的this脱离干系。

<script type="text/javascript">
      foo = "bar";

      function testThis() {
        this.foo = "foo";
      }
     console.log(this.foo); //logs "bar"
     new testThis();
     console.log(this.foo); //logs "bar" 
     console.log(new testThis().foo); //logs "foo" </script>

我更喜欢把新的值称作一个实例。
函数里面的this其实相对比较好理解,如果我们在一个函数里面使用this,需要注意的就是我们调用函数的方式,如果是正常的方式调用函数,this指代全局的this,如果我们加一个new,这个函数就变成了一个构造函数,我们就创建了一个实例,this指代这个实例,这个和其他面向对象的语言很像。另外,写JavaScript很常做的一件事就是绑定事件处理程序,也就是诸如button.addEventListener(‘click’, fn, false)之类的,如果在fn里面需要使用this,this指代事件处理程序对应的对象,也就是button。

prototype this


你创建的每一个函数都是函数对象。它们会自动获得一个特殊的属性prototype,你可以给这个属性赋值。当你用new的方式调用一个函数的时候,你就能通过this访问你给prototype赋的值了。

function Thing() {
       console.log(this.foo);
        }
Thing.prototype.foo = "bar"; 
 var thing = new Thing(); //logs "bar"
console.log(thing.foo);  //logs "bar"

当你使用new为你的函数创建多个实例的时候,这些实例会共享你给prototype设定的值。对于下面的例子,当你调用this.foo的时候,都会返回相同的值,除非你在某个实例里面重写了自己的this.foo

function Thing() {
 }
 Thing.prototype.foo = "bar";
 Thing.prototype.logFoo = function () {
     console.log(this.foo);
  }
  Thing.prototype.setFoo = function (newFoo) {
     this.foo = newFoo;
  }

 var thing1 = new Thing();
 var thing2 = new Thing();

 thing1.logFoo(); //logs "bar"
 thing2.logFoo(); //logs "bar"

 thing1.setFoo("foo");
 thing1.logFoo(); //logs "foo";
 thing2.logFoo(); //logs "bar";

 thing2.foo = "foobar";
 thing1.logFoo(); //logs "foo";
 thing2.logFoo(); //logs "foobar";

实例里面的this是一个特殊的对象。你可以把this想成一种获取prototype的值的一种方式。当你在一个实例里面直接给this添加属性的时候,会隐藏prototype中与之同名的属性。如果你想访问prototype中的这个属性值而不是你自己设定的属性值,你可以通过在实例里面删除你自己添加的属性的方式来实现。

function Thing() {
 }
 Thing.prototype.foo = "bar";
 Thing.prototype.logFoo = function () {
     console.log(this.foo);
 }
 7 Thing.prototype.setFoo = function (newFoo) {
     this.foo = newFoo;
 }
 Thing.prototype.deleteFoo = function () {
     delete this.foo;
 }
 var thing = new Thing();
 thing.setFoo("foo");
 thing.logFoo(); //logs "foo";
 thing.deleteFoo();
 thing.logFoo(); //logs "bar";
 thing.foo = "foobar";
 thing.logFoo(); //logs "foobar";
 delete thing.foo;
 thing.logFoo(); //logs "bar";

或者你也能直接通过引用函数对象的prototype 来获得你需要的值。

function Thing() {
 }
 Thing.prototype.foo = "bar";
 Thing.prototype.logFoo = function () {
    console.log(this.foo, Thing.prototype.foo);
 } 
 var thing = new Thing();
 thing.foo = "foo";
 thing.logFoo(); //logs "foo bar";

通过一个函数创建的实例会共享这个函数的prototype属性的值,如果你给这个函数的prototype赋值一个Array,那么所有的实例都会共享这个Array,除非你在实例里面重写了这个Array,这种情况下,函数的prototype的Array就会被隐藏掉。

function Thing() {
 }
 Thing.prototype.things = []; 
 var thing1 = new Thing();
 var thing2 = new Thing();
 thing1.things.push("foo");
 console.log(thing2.things); //logs ["foo"]

给一个函数的prototype赋值一个Array通常是一个错误的做法。如果你想每一个实例有他们专属的Array,你应该在函数里面创建而不是在prototype里面创建。

function Thing() {
     this.things = [];
  }  
 var thing1 = new Thing();
 var thing2 = new Thing();
 thing1.things.push("foo");
 console.log(thing1.things); //logs ["foo"]
 console.log(thing2.things); //logs []

实际上你可以通过把多个函数的prototype链接起来的从而形成一个原型链,因此this就会魔法般地沿着这条原型链往上查找直到找你你需要引用的值。

 function Thing1() {
 }
 Thing1.prototype.foo = "bar";

 function Thing2() {
 }
Thing2.prototype = new Thing1();
 var thing = new Thing2();
 console.log(thing.foo); //logs "bar"

一些人利用原型链的特性来在JavaScript模仿经典的面向对象的继承方式。任何给用于构建原型链的函数的this的赋值的语句都会隐藏原型链上游的相同的属性。

function Thing1() {
 }
  Thing1.prototype.foo = "bar"; 
  function Thing2() {
       this.foo = "foo";
 }
 Thing2.prototype = new Thing1(); 
 function Thing3() {
 }
 Thing3.prototype = new Thing2();  
 var thing = new Thing3();
 console.log(thing.foo); //logs "foo"

我喜欢把被赋值给prototype的函数叫做方法。在上面的例子中,我已经使用过方法了,如logFoo。这些方法有着相同的prototype,即创建这些实力的原始函数。我通常把这些原始函数叫做构造函数。在prototype里面定义的方法里面使用this会影响到当前实例的原型链的上游的this。这意味着你直接给this赋值的时候,隐藏了原型链上游的相同的属性值。这个实例的任何方法都会使用这个最新的值而不是原型里面定义的这个相同的值。

 function Thing1() {
 }
 Thing1.prototype.foo = "bar";
 Thing1.prototype.logFoo = function () {
     console.log(this.foo);
 }
 function Thing2() {
     this.foo = "foo";
 }
 Thing2.prototype = new Thing1();
 var thing = new Thing2();
 thing.logFoo(); //logs "foo";

在JavaScript里面你可以嵌套函数,也就是你可以在函数里面定义函数。嵌套函数可以通过闭包捕获父函数的变量,但是这个函数没有继承this

function Thing() {
 }
  Thing.prototype.foo = "bar";
  Thing.prototype.logFoo = function () {
      var info = "attempting to log this.foo:";
      function doIt() {
          console.log(info, this.foo);
      }
      doIt();
 }


 var thing = new Thing();
 thing.logFoo();  //logs "attempting to log  this.foo: undefined"

  在doIt里面的this是global对象或者在严格模式下面是undefined。这是造成很多不熟悉JavaScript的人深陷 this陷阱的根源。在这种情况下事情变得非常糟糕,就像你把一个实例的方法当作一个值,把这个值当作函数参数传递给另外一个函数但是却不把这个实例传递给这个函数一样。在这种情况下,一个方法里面的环境变成了全局范围,或者在严格模式下面的undefined。

function Thing() {
 }
 Thing.prototype.foo = "bar";
  Thing.prototype.logFoo = function () {  
      console.log(this.foo);   
  }

  function doIt(method) {
      method();
 }


 var thing = new Thing();
 thing.logFoo(); //logs "bar"
 doIt(thing.logFoo); //logs undefined

一些人喜欢先把this捕获到一个变量里面,通常这个变量叫做self,来避免上面这种情况的发生。

function Thing() {
  }
 Thing.prototype.foo = "bar";
  Thing.prototype.logFoo = function () {
      var self = this;
      var info = "attempting to log this.foo:";
      function doIt() {
          console.log(info, self.foo);
     }
     doIt();
 }


 var thing = new Thing();
 thing.logFoo();  //logs "attempting to log this.foo: bar"

但是当你需要把一个方法作为一个值传递给一个函数的时候并不管用。

function Thing() {
  }
  Thing.prototype.foo = "bar";
  Thing.prototype.logFoo = function () { 
      var self = this;
      function doIt() {
          console.log(self.foo);
      }
      doIt();
 }

 function doItIndirectly(method) {
     method();
 }


 var thing = new Thing();
 thing.logFoo(); //logs "bar"
 doItIndirectly(thing.logFoo); //logs undefined

你可以通过bind将实例和方法一切传递给函数来解决这个问题,bind是一个函数定义在所有函数和方法的函数对象上面。

 1 function Thing() {
 2 }
 3 Thing.prototype.foo = "bar";
 4 Thing.prototype.logFoo = function () { 
 5     console.log(this.foo);
 6 }
 7 
 8 function doIt(method) {
 9     method();
10 }
11 
12 
13 var thing = new Thing();
14 doIt(thing.logFoo.bind(thing)); //logs bar

你同样可以使用apply和call来在新的上下文中调用方法或函数。

 1 function Thing() {
 2 }
 3 Thing.prototype.foo = "bar";
 4 Thing.prototype.logFoo = function () { 
 5     function doIt() {
 6         console.log(this.foo);
 7     }
 8     doIt.apply(this);
 9 }
10 
11 function doItIndirectly(method) {
12     method();
13 }
14 
15 
16 var thing = new Thing();
17 doItIndirectly(thing.logFoo.bind(thing)); //logs bar

你可以用bind来代替任何一个函数或者方法的this,即便它没有赋值给实例的初始prototype。

 1 function Thing() {
 2 }
 3 Thing.prototype.foo = "bar";
 4 
 5 
 6 function logFoo(aStr) {
 7     console.log(aStr, this.foo);
 8 }
 9 
10 
11 var thing = new Thing();
12 logFoo.bind(thing)("using bind"); //logs "using bind bar"
13 logFoo.apply(thing, ["using apply"]); //logs "using apply bar"
14 logFoo.call(thing, "using call"); //logs "using call bar"
15 logFoo("using nothing"); //logs "using nothing undefined"

你应该避免在构造函数里面返回任何东西,因为这可能代替本来应该返回的实例。

1 function Thing() {
 2     return {};
 3 }
 4 Thing.prototype.foo = "bar";
 5 
 6 
 7 Thing.prototype.logFoo = function () {
 8     console.log(this.foo);
 9 }
10 
11 
12 var thing = new Thing();
13 thing.logFoo(); //Uncaught TypeError: undefined is not a function

奇怪的是,如果你在构造函数里面返回了一个原始值,上面所述的情况并不会发生并且返回语句被忽略了。最好不要在你将通过new调用的构造函数里面返回任何类型的数据,即便你知道自己正在做什么。如果你想创建一个工厂模式,通过一个函数来创建一个实例,这个时候不要使用new来调用函数。当然这个建议是可选的。

你可以通过使用Object.create来避免使用new,这样同样能够创建一个实例。

 1 function Thing() {
 2 }
 3 Thing.prototype.foo = "bar";
 4 
 5 
 6 Thing.prototype.logFoo = function () {
 7     console.log(this.foo);
 8 }
 9 
10 
11 var thing =  Object.create(Thing.prototype);
12 thing.logFoo(); //logs "bar"

在这种情况下并不会调用构造函数

 1 function Thing() {
 2     this.foo = "foo";
 3 }
 4 Thing.prototype.foo = "bar";
 5 
 6 
 7 Thing.prototype.logFoo = function () {
 8     console.log(this.foo);
 9 }
10 
11 
12 var thing =  Object.create(Thing.prototype);
13 thing.logFoo(); //logs "bar"

因为Object.create不会调用构造函数的特性在你继承模式下你想通过原型链重写构造函数的时候非常有用。

 1 function Thing1() {
 2     this.foo = "foo";
 3 }
 4 Thing1.prototype.foo = "bar";
 5 
 6 function Thing2() {
 7     this.logFoo(); //logs "bar"
 8     Thing1.apply(this);
 9     this.logFoo(); //logs "foo"
10 }
11 Thing2.prototype = Object.create(Thing1.prototype);
12 Thing2.prototype.logFoo = function () {
13     console.log(this.foo);
14 }
15 
16 var thing = new Thing2();

原博客连接:
[https://segmentfault.com/a/1190000002640298#articleHeader3]

内容概要:本文详细介绍了个基于Java与Vue的食品安全溯源与智能分析系统的设计与实现,涵盖项目背景、目标意义、面临挑战及解决方案,并阐述了系统的整体架构与核心技术模块。系统通过集成物联网设备实现全流程数据采集,采用分布式数据库保障大数据存储与高效访问,结合机器学习算法进行风险预测与智能预警,同时利用可视化技术呈现溯源链路与分析结果,实现了食品从生产到销售全过程的透明化、智能化管理。文中还提供了关键模块的代码示例,如数据清洗、特征提取、决策树模型训练与预测、溯源接口开发等,增强了项目的可实施性与参考价值。; 适合人群:具备Java开发基础、熟悉Spring Boot和Vue框架,有定前后端开发经验的软件工程师或计算机专业学生,尤其适合从事食品安全、物联网、大数据分析等相关领域技术研发的人员; 使用场景及目标:①构建食品全链条溯源体系,提升企业对食品安全事件的快速响应能力;②实现生产流程数字化管理,支持政府监管与消费者透明查询;③应用机器学习进行风险建模与智能预警,推动食品行业智能化转型; 阅读建议:建议结合文中提供的模型描述与代码示例,深入理解各模块设计逻辑,重点关注数据处理流程、算法实现与前后端交互机制,可基于该项目进行二次开发或拓展应用于其他行业的溯源系统建设。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值