原文:你不知道的js系列

JavaScript 的 this 机制并没有那么复杂

为什么会有 this?

在如何使用 this 之前,我们要搞清楚一个问题,为什么要使用 this。

下面的代码尝试去说明 this 的使用动机:

function identify() {
return this.name.toUpperCase();
} function speak() {
var greeting = "Hello, I'm " + identify.call( this );
console.log( greeting );
} var me = {
name: "Kyle"
}; var you = {
name: "Reader"
}; identify.call( me ); // KYLE
identify.call( you ); // READER speak.call( me ); // Hello, I'm KYLE
speak.call( you ); // Hello, I'm READER

这段代码使得函数 identify() 和 speak() 可以在多个上下文(me 和 you)对象中重用,不用给每个对象分别创建函数。

如果不用 this,你也可以将上下文对象直接传入函数:

function identify(context) {
return context.name.toUpperCase();
} function speak(context) {
var greeting = "Hello, I'm " + identify( context );
console.log( greeting );
} identify( you ); // READER
speak( me ); // Hello, I'm KYLE

然而 this 机制可以隐式地传递一个对象引用,使得 API 设计得更简洁和更容易复用。

你的使用模式越复杂,你就能更加明白,显式传递一个参数经常比传递 this 上下文还混乱。

困惑

在解释 this 如何工作之前,必须要先摒弃错误的概念。开发者们总是太过依赖 this 的字面意思。

引用自身 Itself

一种普遍的错误是认为 this 指代这个函数自身。

为什么你会想从一个函数内部引用它自己呢,通常的原因是递归,或者事件回调函数在被调用之后解除绑定。

JS 新手会认为将函数作为对象引用可以在函数调用期间存储状态(属性的值)。这确实是可以的但是用处有限,后面会介绍其它模式,除了函数对象本身还有更好的存储状态的地方。

下面的代码会说明,this并不会像我们以为的那样让函数得到对自身的引用:

function foo(num) {
console.log( "foo: " + num ); // keep track of how many times `foo` is called
this.count++;
} foo.count = 0; var i; for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9 // how many times was `foo` called?
console.log( foo.count ); // 0 -- WTF?

foo.count 还是 0 ,循环确实执行了 4 次,console.log 也确实被调用了 4 次。

foo.count = 0 执行之后,实际上给函数对象 foo 添加了一个属性 count。

但是在函数内部的 this.count 中,this 实际上并不指向这个函数对象,即使这个属性名字是一样的,但属性所在的对象是不同的。

如果 foo 的属性 count 的值没有改变,那么我们改变的究竟是什么。实际上,如果你再深究一下,就会发现,这段代码意外地创建了一个全局变量 count,而且当时会有一个值 NaN(具体看这个系列的第二节)。

很多开发者就会通过别的方式避免这个问题,比如创建另外一个对象储存这个属性 count:

function foo(num) {
console.log( "foo: " + num ); // keep track of how many times `foo` is called
data.count++;
} var data = {
count: 0
}; var i; for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9 // how many times was `foo` called?
console.log( data.count ); //

这确实解决了问题,但是很遗憾这忽略了真正的问题——不理解 this 的含义和用法,只是回到熟悉的词法作用域机制。

如果想在一个函数对象内部引用自身,this 是不够的,你需要一个标识符:

function foo() {
foo.count = 4; // `foo` refers to itself
} setTimeout( function(){
// anonymous function (no name), cannot
// refer to itself
}, 10 );

在第一个函数中,函数被命名为 foo,这个标识符 foo 就可以用来指代这个函数对象自身。

但在第二段中,回调函数没有名字,所以没办法引用自己。

注:老派的已经被废弃的 arguments.callee 在函数中可以用来指代正在执行的函数对象。这是在匿名函数内部访问函数对象的唯一方式。

当然最好的方式还是避免匿名函数的使用。

另外一种解决办法就是使用 foo 标识符,不使用 this:

function foo(num) {
console.log( "foo: " + num ); // keep track of how many times `foo` is called
foo.count++;
} foo.count = 0; var i; for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9 // how many times was `foo` called?
console.log( foo.count ); //

然而这种方法同样回避了对 this 的理解。

另外一种解决这个问题的方式是,将 this 强制绑定到 foo 这个函数对象上:

function foo(num) {
console.log( "foo: " + num ); // keep track of how many times `foo` is called
// Note: `this` IS actually `foo` now, based on
// how `foo` is called (see below)
this.count++;
} foo.count = 0; var i; for (i=0; i<10; i++) {
if (i > 5) {
// using `call(..)`, we ensure the `this`
// points at the function object (`foo`) itself
foo.call( foo, i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9 // how many times was `foo` called?
console.log( foo.count ); //

作用域的引用 Its Scope

第二个常见的关于 this 的错误理解是,this 指向这个函数的作用域。这是一个有点狡猾的问题,因为在某种意义上这种说法是有些正确的,但在另一种意义上,这又是被误导的。

首先,this 并没有指向函数的词法作用域。作用域确实就像是一个包含所有标识符属性的对象,但是这个作用域 “对象” 是无法被代码直接访问的,这是引擎内部实现的。

所以下面的代码是错误的:

function foo() {
var a = 2;
this.bar();
} function bar() {
console.log( this.a );
} foo(); //undefined

你可能觉得这段代码很做作,但这是摘自一些帮助论坛里的真实代码。

首先,这段代码试图通过 this.bar() 引用函数 bar(),能运行起来也是巧合。调用 bar() 最自然的方式就是直接使用标识符引用,去掉前面的 this。

然而,写这段代码的开发者其实是想让 bar() 访问 foo() 内部的变量 a,但 this 不能被用来查询词法作用域的。

this 到底是什么

前面讲到过,this 是在运行时绑定的,它的上下文环境取决于函数调用的条件。this 的绑定和函数声明的位置没有关系,和函数调用的位置有关。

当一个函数被调用时,一个执行上下文被创建。这个上下文记录包含函数调用的位置,函数调用的方式以及传入的参数这些信息。this 的引用就是在这个时候决定的。

在下一节中,会介绍根据一个函数的调用位置确定它执行过程中将如何绑定this。

小结:

  • this 既不指代函数本身,也不指代函数的词法作用域。
  • this 是在函数调用的时候绑定的,它引用的内容完全取决于函数调用的位置。

你不知道的JS之 this 和对象原型(一)this 是什么的更多相关文章

  1. 关于js的对象原型继承(一)

    javascript中,对象的继承是通过原型去继承. 可以这样理解:js中的对象,包含的除了属性和方法,还有一个最基本的原型__proto__对象.这个原型__proto__指向谁,这个对象就继承谁. ...

  2. jquery实现点击展开列表同时隐藏其他列表 js 对象操作 对象原型操作 把一个对象A赋值给另一个对象B 并且对象B 修改 不会影响 A对象

    这篇文章主要介绍了jquery实现点击展开列表同时隐藏其他列表的方法,涉及jquery鼠标事件及节点的遍历与属性操作技巧,具有一定参考借鉴价值,需要的朋友可以参考下 本文实例讲述了jquery实现点击 ...

  3. JS的对象原型

    1.对象 1.1 语法 对象可以通过两种形式定义:声明(文字)形式和构造形式. 对象的文字语法: var myObj = { key : value //... }; 对象的构造语法: var myO ...

  4. 关于JS对象原型prototype与继承,ES6的class和extends · kesheng's personal blog

    传统方式:通过function关键字来定义一个对象类型 1234567891011 function People(name) { this.name = name}People.prototype. ...

  5. 读书笔记-你不知道的JS上-混入与原型

    继承 mixin混合继承 function mixin(obj1, obj2) { for (var key in obj2) { //重复不复制 if (!(key in obj1)) { obj1 ...

  6. 深度剖析前端JavaScript中的原型(JS的对象原型)

          这张图片有点劝退了,哈哈哈~    通过原型机制,JavaScript 中的对象从其他对象继承功能特性:这种继承机制与经典的面向对象编程语言的继承机制不同.本文将探讨这些差别,解释原型链如 ...

  7. 你不知道的JS之作用域和闭包 附录

     原文:你不知道的js系列 A 动态作用域 动态作用域 是和 JavaScript中的词法作用域 对立的概念. 动态作用域和 JavaScript 中的另外一个机制 (this)很相似. 词法作用域是 ...

  8. JS基础-该如何理解原型、原型链?

    JS的原型.原型链一直是比较难理解的内容,不少初学者甚至有一定经验的老鸟都不一定能完全说清楚,更多的"很可能"是一知半解,而这部分内容又是JS的核心内容,想要技术进阶的话肯定不能对 ...

  9. js之oop <二> 对象属性

    js中对象属性可以动态添加和删除.删除对象属性用delete关键字. function obj(){ } var oo = new obj(); oo.a = "a"; oo.b ...

随机推荐

  1. JSR-303 数据校验学习

    一.JSR-303简介JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation,官方参考实现是Hibernate Validator. 此实现与 Hibernate ...

  2. anylogic 常用模块及术语的使用

    1.1 存量 创建存量变量 1. 在调色板视图下的Model页面中选择Stock 元素. 2. 在图形化编辑器中您所期望的位置点击鼠标,放置该存量. 3. 在属性视图下的Equation部分中,定义该 ...

  3. kafka 重新分配partition

    登陆kafka manager 进入相关topic 点击generate partition assignments 点击reassign partirons

  4. (转)理解maven命令package、install、deploy的联系与区别

    我们在用maven构建java项目时,最常用的打包命令有mvn package.mvn install.deploy,这三个命令都可完成打jar包或war(当然也可以是其它形式的包)的功能,但这三个命 ...

  5. Android6.0以上系统动态获取权限

    动态权限的申请方法: 1.首先,需要在AndroidManifest.xml静态申请权限,否则无法动态申请权限: <uses-permission android:name="andr ...

  6. 算法工程师<数学题/智力题>

    <数学题/智力题> 1.如果一个女生说,她集齐了十二个星座的前男友,我们应该如何估计她前男友的数量? https://blog.csdn.net/FnqTyr45/article/deta ...

  7. C++—模板(1)模板与函数模板

    1.引入 如何编写一个通用加法函数?第一个方法是使用函数重载, 针对每个所需相同行为的不同类型重新实现这个函数.C++的这种编程机制给编程者极大的方便,不需要为功能相似.参数不同的函数选用不同的函数名 ...

  8. Centos设置防火墙与开放访问端口

    一. jeuxs在启动后可能会出现启动jexus成功,但是访问失败.但是在服务器内部访问没问题. 列出所有端口 netstat -ntlp 查看已经开放的端口: firewall-cmd --list ...

  9. jdbc crud

    最近在做一个mybatis的sql审计,所有需要原生的使用一下jdbc,基于次,复习一下自己的基础知识 github 地址: https://github.com/warriorg/nodes/tre ...

  10. 大数据学习之HDFS基本API操作(下)06

    hdfs文件流操作方法一: package it.dawn.HDFSPra; import java.io.BufferedReader; import java.io.FileInputStream ...