JavaScript中的this
发布于 7 年前 作者 hiddaorear 2956 次浏览 来自 分享

原文地址

JavaScript中的this

原理

错误的this指向

通常所说的:如果是全局环境中,this指向全局对象,如果是对象的方法,这this指向这个对象。

例子1:


var foo = {
  bar: function() {
    console.log(this)
  }
}

foo.bar();
(foo.bar)();

(foo.bar = foo.bar)();
(false || foo.bar)();
(foo.bar, foo.bar)();

例子1前两者为foo,后面都是全局对象。后三者并没有指向foo。所以我们上面的通常说法不精确。

精确的this指向

在全局环境中,this指向全局对象。而在普通函数调用中,this是由激活上下文的调用者提供,即调用这个函数的父作用域,以及函数调用的语法形式,决定了this的值,这是一个动态可变的值。

例子2:


var foo = {
  bar: function() {
    console.log(this)
    console.log(this === foo)
  }
}

foo.bar() // foo, true

var fn = foo.bar

console.log(fn === foo.bar) // true

fn() // global, false

例子2中,第一次调用指向foo,把foo.bar赋值给fn之后,this没有指向foo。是什么导致this指向的变化呢?

this指向的内部原理

this是执行上下文的一个属性:


activeExecutionContext = {
  VO: {...},
  this: thisValue
}

在普通函数调用中,this是由激活上下文的调用者提供,即调用这个函数的父作用域,函数调用的语法形式,决定了this的值,这是一个动态可变的值。

为什么会引起这个差异呢? 因为引用类型的不同处理,是否会获取真实的值,所导致的。

引用类型存在形式:

1 标识符(变量名,函数名,函数参数名,全局对象属性名)

2 属性访问器(foo.bar(); foo['bar'](), 点标记法;可以动态设置属性名的方括号[]

为了从引用类型中获取真实的值,存在类似getValue的方法。而函数上下文的规则是,函数上下文中this由调用者提供,并由调用形式决定。如果调用的圆括号左侧是一个引用类型,this为这个引用类型,如果是非引用类型,这为null,但为null无意义,被隐式转化为全局对象。

为什么有this的特性?

this是一个指针,便于代码的更为简洁地复用。


// 无this
function upper(context) {
  return context.name.toUpperCase()
}
function speak(context) {
  var greeting = "Hello, I'm " + upper(context)
  console.log(greeting)
}

var me = {
  name: 'm'
}

var you = {
  name: 'y'
}

speak(me)

// 利用this

function upper() {
  return this.name.toUpperCase()
}
function speak() {
  var greeting = "Hello, I'm " + upper.call(this)
  console.log(greeting)
}

speak.call(me)


这里this可以简化上下文对象的传递。其他OPP语言中this关键字和OPP密切相关,一般是引用刚创建的对象,但在ECMAScript中,this只限于引用创建过的对象,this的指向和函数调用形式有关,不一定引用类型调用就指向引用类型。

this指向的改变

1 构造函数中的this


function C() {
  console.log(this)
  this.x = 10
}

var a = new C()
console.log(a.x);

new操作符会调用函数的内部的Construct方法,创建对象,之后调用函数的Call方法,把新创建对象作为this值。

2 调用函数时call与apply设置this的值


var b = 10
function a(c) {
  console.log(this.b)
  console.log(c)
}

a(20)
a.call({b: 20}, 30)
a.apply({b: 20}, [40])

call, apply, bind以及箭头函数

call,apply,bind皆为动态的改变this指针的方法。其中call和apply是当Object没有某个方法,但是其它对象有,可以借助call和apply改变this的指向,调用其它对象的方法。bind为绑定this为某个对象。

典型的应用:

将类数组元素转化为数组: Array.prototype.slice.apply(document.getElementsByTagName('*'))

检查类型:


function isArray(obj) {
  return Object.prototpye.toString.call(obj) === '[object Array]'
}

箭头函数则与前三者不同。 If kind is Arrow, set the [[ThisModel]] internal slot of F to lexical.If the value is “lexical”, this is an ArrowFunction and does not have a local this value. If thisModel is lexical, return NormalCompletion(undefined).

箭头函数没有自己的this绑定,同时在函数执行时绑定this会被直接忽略。其中this总是指向定义时所在的对象,而不是运行时所在的对象。即箭头函数的this值是lexical scope 的this值。这一特性使得箭头函数在React中的render函数中使用起来很方便。


function foo() {
  setTimeout(() => {
    console.log('id: ', this.id)
  }, 100)
}

var id = 0

foo.call({id: 42})

// 容易误解的地方
// {id: 42}
// 是箭头函数定义所在的对象还是运行时所在的对象。由于箭头函数位于foo函数内部,只有foo函数运行之后他才会生成,所以foo运行时所在的对象,即箭头函数定义所在的对象。


var f = () => 5;
// 近似等价于
var f = function() {return 5;}.bind(this);

综上,call,apply,bind使得JavaScript具有动态改变this的特性,而箭头函数使得JavaScript具有固定this的指向的特性。一动一静,相得益彰。

在编程中的运用

ES7中的::


  this.x = 0
  let module = {
    x: 1,
    getX: function() {
      console.log(this.x)
    }
  }
  module.getX()
  let get = module.getX
  get() // 0
  let boundGetX = get.bind(module)
  boundGetX() // 1
  let ES7boundGetx = module::get
  ES7boundGetx() // 1

super


class P {
  foo() {
    console.log('P.foo')
  }
}

class C extends P {
  foo() {
    super.foo()
  }
}

var c1 = new C()
c1.foo() // P.foo

var D = {
  foo: function() {
    console.log('D.foo')
  }
}

var E = {
  foo: C.prototype.foo
}

Object.setPrototypeOf(E, D)
E.foo() // P.foo


可见super的绑定是静态绑定,创建时即完成绑定。所以E委托了D,但并不能调用到D.foo(),类似于箭头的函数的this绑定。

jQuery中的this

链式调用的实现;


function Constructor() {
  this.art = 0
}

Constructor.prototype.fn_0 = function() {
  console.log('0')
  return this;
}

Constructor.prototype.fn_1 = function() {
  console.log('1')
  return this;
}

new Constructor().fn_0().fn_1()

调用的方法返回this即可。

end()的实现


function end() {
  return this.prevObject || this.constructor(null)
}

// 设置preObject的函数
function pushStack( ele ) {
  // Build a new jQuery macthed element set
  var ret = jQuery.merge( this.constructor(), elems);
  ret.prevObject = this // ret.pervObject 设置为当前jQuery对象引用
  ret.context = this.context
  return ret;
}

pushStack函数在很多涉及DOM操作的函数都有调用,用于缓存了当前的this。由于只存储当前,所以这里只需要一个preObject即可,无需放在一个数组里。

利与弊

this是JavaScript特性之一,具有脚本语言的动态特性,带来很多便捷,同时由于super和箭头函数的特性,使得this具有了静态的特性,在这两种情况下,this是固定且无法改变的。其利与弊都是this的灵活,双刃剑。所以才有了ES2015中super和箭头函数的固定this的特性。

拾遗

this可被重新赋值么?(不能,this是保留字)

问题(答案见原文)

1 call参数为null时,this的指向


function a() {
  console.log(this)
}
a.call(null)

2 调用形式对this的影响


var foo = {
  bar: function() {
    console.log(this)
  }
}

foo.bar();
(foo.bar)();

(foo.bar = foo.bar)();
(false || foo.bar)();
(foo.bar, foo.bar)();

参考资料:

《你所不知道的JavaScript(上卷)》

关于JavaScript的执行域,标识符解析,闭包的研究

深入ECMA-262-3 第三章、This

JavaScript内部原理实践——真的懂JavaScript吗?

回到顶部