前端技术更新很快,几个月前我还在写React,现在又有人建议我学学Vue了。思考之后决定先沉下心来补补JavaScript基础。You Dont Know JS一系列书不错。这一系列博客是我读这本书以后总结的干货。
知乎的习惯是先问是什么,再问为什么,到这篇博客却要翻过来讲。如果没有需要使用this的场景,那我们就没必要知道this到底是什么了。 请看下面的例子:
// 使用this的动机 function tellName(){ console.log('name is', this.name); } let personOne = { name: 'Claire' } let personTwo = { name: 'LYY' } tellName.call(personOne); tellName.call(personTwo);控制台打印结果:
从以上this的使用中可以发现,我们在写代码时候往往希望一个函数在不同环境下行为风格一致但是具体表现不一样,也就是使用当前环境下的同名变量进行相同工序的“加工”,变量的具体值不同但“加工”工序相同。当然可以选择把这个“环境”作为参数传入函数,但this是一个更优雅的选择。
我们现在模糊知道this是一个和调用环境相关的东西,但是很多时候我们还是不知道如何确定this。在说明如何确定本次调用this之前,请明确以下几点。
this不是编写时候绑定的,而是运行时绑定的,它依赖于函数调用的上下文条件,也就是说this与函数声明的位置无关,反而和函数被调用的方式有关当一个函数被调用时,会建立一个活动记录,被称为执行环境。这个记录包含函数是从何处(call-stack, 调用栈)被调用的,函数是如何被调用的,被传递了什么参数等信息,其中还包括了函数执行期间将被使用的this引用下面是一个使用this的例子:
// 通用例子 function foo(num){ console.log('foo: ' + num); //尝试追踪foo被调用的次数 this.count++; } foo.count = 0; var i; for(i=0; i<10; i++){ if(i > 5){ foo(i); } } console.log('The times foo is called: ', foo.count);下面进行讲解确定this的规则。
一个完全独立的函数(既不是构造函数也不是对象的方法,直接通过function关键字声明),在'use strict'模式下this为undefined,非严格模式下this绑定到window上。举个小例子:
function findThis(){ console.log(this); } findThis();打印结果:
"use strict"; function findThis(){ console.log(this); } findThis();打印结果:
现在我们可以分析一下上一小节中通用例子打印了什么。上一节例子直接用function声明,适用Default Binding规则,分两种情况:
严格模式下,foo中的this为undefined,程序会报错,毕竟undefined不能执行++操作非严格模式下,foo中的this为window,window下没有声明count这个变量,会自动赋值window.count = NaN,程序可以执行,但是在执行++操作的是window.count而不是foo.count,所以打印结果为0,见下图如果把foo中的this.count++改为foo.count++就可以打印出4啦。
隐含绑定:调用该函数时,该函数是否拥有一个环境对象(Context Object)。 要理解这句话,首先得知道函数在何时被调用,这就涉及到两个概念——调用点(call-site)和调用栈(call-stack)。调用点指函数在代码中被调用的位置(不是被声明的位置)。调用栈指使我们到达当前执行位置而被调用的所有方法的堆栈。原文中举了如下例子:
function baz() { // call-stack is: `baz` // so, our call-site is in the global scope console.log( "baz" ); bar(); // <-- call-site for `bar` } function bar() { // call-stack is: `baz` -> `bar` // so, our call-site is in `baz` console.log( "bar" ); foo(); // <-- call-site for `foo` } function foo() { // call-stack is: `baz` -> `bar` -> `foo` // so, our call-site is in `bar` console.log( "foo" ); } baz(); // <-- call-site for `baz`很容易理解,复杂情况下人工分析调用点和调用栈可能比较难,可以借助debug进行。
隐含绑定:调用该函数时,该函数是否拥有一个环境对象(Context Object)。这里的调用函数时,就是函数的调用点。 看下面的代码:
function findThis(){ console.log(this.name); } var name = 'window'; var context = { name: 'context', findThis: findThis }; context.findThis(); //context但是,请注意以下两种情况:
链式调用 function findThis(){ console.log(this.name); } var name = 'window'; var contextOne = { name: 'one', findThis: findThis }; var contextTwo = { name: 'two', context: contextOne } //仅离函数最近的对象为其调用环境 contextTwo.context.findThis(); //one 函数赋值给变量 function findThis(){ console.log(this.name); } var name = 'window'; var contextOne = { name: 'one', findThis: findThis }; var newFindThis = contextOne.findThis; //赋值以后调用环境丢失,降级为default binding newFindThis(); //window为了避免调用环境丢失,常常进行明确绑定。可以使用call, apply, bind或者自己写一个工具函数。bind与call和apply的区别在于,bind在定义函数时候就绑定this,call和apply在调用函数时候才绑定this,故而bind更不容易被更改,在书中被称为Hard Binding,是Explicit Binding的变种。
另外,一些API中会隐含地使用以上函数,比如:
var context = { name: 'context' }; [1, 2, 3].map(function(item){console.log(item, this.name)}, context);打印结果:
JavaScript中构造函数就是一个普通的函数。当一个函数前被使用new调用时就称为构造函数。new调用会自动完成以下工作:
一个全新的对象会凭空创建(就是被构建)这个新构建的对象会被接入原形链([[Prototype]]-linked)这个新构建的对象被设置为函数调用的this绑定除非函数返回一个它自己的其他 对象,这个被new调用的函数将 自动 返回这个新构建的对象 function Person(name){ this.tellName = function(){ console.log(name); }; } var lyy = new Person('lyy'); lyy.tellName(); //lyy当一次调用符合以上四条规则的多条时候,按照以下优先级确定this:
被new调用?使用新构建的对象。被call或apply(或 bind)调用?使用指定的对象。被持有调用的环境对象调用?使用那个环境对象。默认:strict mode下是undefined,否则就是全局对象。