原文:你不知道的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. JAVA_Sprint学习(一)

    保存用户信息的编程思维 传统的思想,就是建立一个类之后,然后将用户的姓名和密码,以及添加用户等操作都放在一个main中, 按照抽象编程的思想而言, 首先建立一个类User,用来是表示用户的具体信息Us ...

  2. LNMP安装目录及配置文件位置

    LNMP相关软件安装目录 Nginx 目录: /usr/local/nginx/ MySQL 目录 : /usr/local/mysql/MySQL数据库所在目录:/usr/local/mysql/v ...

  3. <A>标记onclick事件

    <script> function ss() { document.getElementById("btnPublicity").click(); }</scri ...

  4. Java字符串中常用字符占用字节数

    java中一个char型的数据(也就是一个字符)占两个字节.而Java中常用的字符包括数字.英文字母.英文符号.中文汉字.中文符号等,若在字符串中包含里面的多种字符,它们是否都占两个字符呢?答案是否定 ...

  5. (原创)动态内存管理练习 C++ std::vector<int> 模拟实现

    今天看了primer C++的 “动态内存管理类”章节,里面的例子是模拟实现std::vector<std::string>的功能. 照抄之后发现编译不通过,有个库函数调用错误,就参考着自 ...

  6. form 组件

    https://www.cnblogs.com/wupeiqi/articles/6144178.html class F2Form(forms.Form): title1=fields.CharFi ...

  7. iOS端临近封包时要做哪些事情?

    iOS封包前的注意事项: 0.功能测试,打点测试都已OK 1.创建case,使用master执行此轮case,修改版本号 2.建议使用各个系统的机型,如8,9,10,11,12, iPad等 3.ma ...

  8. spring-cloud-hystrix服务熔断与降级

    Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix能保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联 ...

  9. LoadRunner基础知识

    什么是自动化性能测试?利用产品.人员和流程来降低应用程序.升级程序或补丁程序部署风险的一种手段 什么是自动化性能测试的核心?向预部署系统施加工作负载,同时评估系统性能和最终用户体验 LoadRunne ...

  10. layer.open()利用代码实现伪阻塞

    今天在项目中遇到需要弹框处理的问题,,当用户点击某个单选框时,需要进行确认操作,,常规的情况下,因为layer.open()和layer.confirm()都是异步执行的, 在点击单选框之后单选框会立 ...