this你到底是谁

135 阅读7分钟

首先,为什么要有 this?

js中this的用法

js中this的原理

请看下面的例子,定义了一个 person 对象,有 name 和 age 属性,还定义了一个 sayHi 方法。试想,如果现在没有 this 的话,我想在 syaHi 中打印出 person 的 name 和 age,那么就需要把这个对象的 name 和 age 属性作为参数传递进去。如图一。 进行第一次改进,将对象 person 作为参数直接传进去,如图二。 进行第二次改进,能不能在调用的时候,不传这个 person,因为毕竟前面使用 person.sayHi(),就是要用这个 person 的 name 和 age。

此时有两个方法:

1》依然把第一个参数 self 当作 person,形参要始终比实参多一个,self

2》隐藏 self,用关键字 this 来访问 self

JS 之父选择了方法 2,Python 之父选择了方法 1,留下 self 作为第一个参数

而实际上 this 就是隐藏的第一个形参。在调用 person.sayHi() 时,这个 person 会「变成」 this。

即 person.sayHi()其实等价于 person.sayHi.call(person)。.call 的第一个参数就是显式的 person,没有任何语法糖。因此你可以使用 obj.fn.call(null,1,2,3)来手动禁用 this。这样一来,person.sayHi.call 的参数其实可以是任何对象,也就是说虽然 person.sayHi 虽然是 person 的方法,但其实是可以调用在任何对象上的。

就像下图阮老师博客里写到的:由于函数可以运行于不同的环境里,需要有一种机制,能够在函数体内部获得当前的运行环境。所以this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。

接下来看一下函数调用:

一般函数调用有这三种情况 1》fn(p1,p2) 2》obj.fn(p1,p2) 3》fn.call(context,p1,p2) 只有第三种调用形式,才是正常的调用形式,其余两种都可以等价的变为 call 形式: fn(p1,p2) fn.call(undefined,p1,p2) obj.fn(p1,p2) obj.fn.call(undefined,p1,p2) 到此,我们的函数调用只有一种形式: fn.call(context,p1,p2) 所以 this 就是 context!!!

this,就是上面代码中的 context。就这么简单。 即换句话说就是 this 永远指向最后调用它的那个对象

先看 func(p1, p2) 中的 this 如何确定:

function func(){ console.log(this) } func() 用「转换代码」把它转化一下,得到

function func(){ console.log(this) } func.call(undefined) // 可以简写为 func.call() 按理说打印出来的 this 应该就是 undefined 了吧,但是浏览器里有一条规则: 如果你传的 context 是 null 或 undefined,那么 window 对象就是默认的 context(严格模式下默认 context 是 undefined) 因此上面的打印结果是 window。 如果你希望这里的 this 不是 window,很简单: func.call(obj) // 那么里面的 this 就是 obj 对象了

再看 obj.child.method(p1, p2) 的 this 如何确定

var obj = { foo: function(){ console.log(this) } } obj.foo() 按照「转换代码」,我们将 obj.foo() 转换为 obj.foo.call(obj) 好了,this 就是 obj。搞定。

[ ] 语法中 this 怎么确定

function fn (){ console.log(this) } var arr = [fn, fn2] arr0 // 这里面的 this 又是什么呢? 我们可以把 arr0 想象为 arr.0( ),虽然后者的语法错了,但是形式与转换代码里的 obj.child.method(p1, p2) 对应上了,于是就可以愉快的转换了: arr0 假想为 arr.0() 然后转换为 arr.0.call(arr) 那么里面的 this 就是 arr 了 :)

箭头函数

实际上箭头函数里并没有 this,如果你在箭头函数里看到 this,你直接把它当作箭头函数外面的 this 即可。外面的 this 是什么,箭头函数里面的 this 就还是什么,因为箭头函数本身不支持 this。 有人说「箭头函数里面的 this 指向箭头函数外面的 this」,这很傻,因为箭头函数内外 this 就是同一个东西,并不存在什么指向不指向。

作为构造函数调用(看后面new的解释)

所谓构造函数,就是通过这个函数,可以生成一个新对象。这时,this就指这个新对象。

function test() {
 this.x = 1;
}

var obj = new test();
obj.x // 1

总结 this 就是你 call 一个函数时,传入的第一个参数。(「this 就是 call 的第一个参数」) this 永远指向最后调用它的那个对象 如果你的函数调用形式不是 call 形式,请按照「转换代码」将其转换为 call 形式。

Event Handler 中的 this btn.addEventListener('click' ,function handler(){ console.log(this) // 请问这里的 this 是什么 }) handler 中的 this 是什么呀,看了你的文章我还是不懂啊? 那是因为你没有看懂,我们说过 this 都是由 call 或 apply 指定的,那么你只需要找到 handler 被调用时的代码就行了。 可是我哪知道 addEventListener 的源代码呀。是呀,addEventListener 是浏览器内置的方法,我们看不见源代码。 所以,你只能看文档了,MDN 这样说:通常来说 this 的值是触发事件的元素的引用,这种特性在多个相似的元素使用同一个通用事件监听器时非常让人满意。当使用 addEventListener() 为一个元素注册事件的时候,句柄里的 this 值是该元素的引用。其与传递给句柄的 event 参数的 currentTarget 属性的值一样。由于浏览器知道你不方便看源码里是怎么 call handler 的,所以直接在文档里告诉你了,你可以假想浏览器的源码是这样写的: // 当事件被触发时 handler.call(event.currentTarget, event) // 那么 this 是自然就知道了

jQuery Event Handler 中的 this 那么下面代码中的 this 是什么呢: $ul.on('click', 'li' , function(){ console.log(this) })

看 jQuery 源码是怎么 call 这个函数的,或者看 jQuery 文档。jQuery 文档是这样写的:当 jQuery 的调用处理程序时,this 关键字指向的是当前正在执行事件的元素。对于直接事件而言,this 代表绑定事件的元素。对于代理事件而言,this 则代表了与 selector 相匹配的元素。(注意,如果事件是从后代元素冒泡上来的话,那么 this 就有可能不等于 event.target。)若要使用 jQuery 的相关方法,可以根据当前元素创建一个 jQuery 对象,即使用 $(this)。

总结一下如何确定 this 是值 看源码中对应的函数是怎么被 call 的(这是最靠谱的办法);看文档;console.log(this);千万不要瞎猜,猜不到的

如何强制指定 this 的值?自己写 call / apply 即可

new是用来干什么的

new其实可以看作一个语法糖,要造100个矩形

    let cat = {
        id:1,
        run:function(){/*走*/}
        eat:function(){/*吃东西*/}
    }
    

如果要写100个cat呢?像下面利用for循环。nonono,浪费了太多资源,因为每个cat的run和eat是相同的。

let cats = []
 for(let i=0;i<100;i++){
	let cat = {
       id:i,
        run:function(){/*走*/},
        eat:function(){/*吃东西*/}
    }
    cats.push[cat]
 }
 

改进:用原型链可以解决重复创建的问题:我们先创建一个「catprototype」,然后让「cat」的 proto 指向「catprototype」

let catprototype = {
	 run:function(){/*走*/},
     eat:function(){/*吃东西*/}
}

let cats = []
for(let i=0;i<100;i++){
        let cat = {
           id:i,
        }
        cat.__proto__ = catprototype
        cats.push[cat]
 }
  

继续改进:用一个函数把这两部分联系起来:

function Cat(i){
  var 临时对象 = {} //***********1  new可以创建临时对象
  临时对象.__proto__ = cat.原型 //***********2  new绑定原型
  cat.id = i
  return 临时对象 //***********3  new会返回该对象
}

Cat.原型 = { //***********4 new会将原型统一称为prototype
  run:function(){/*走*/},
  eat:function(){/*吃东西*/}
}

// 保存为文件:cat.js

然后就可以愉快地引用「Cat」来创建cat了: var cats= [] for(var i=0; i<100; i++){ cats.push(cat(i)) }

只要在Cat前面使用 new 关键字,那么可以少做四件事情:

不用创建临时对象,因为 new 会帮你做(你使用「this」就可以访问到临时对象);
不用绑定原型,因为 new 会帮你做(new 为了知道原型在哪,所以指定原型的名字为 prototype);
不用 return 临时对象,因为 new 会帮你做;
不要给原型想名字了,因为 new 指定名字为 prototype。

这一次我们用 new 来写

function Cat(i){
  this.id = i
}

Cat.prototype = {
 run:function(){/*走*/},
 eat:function(){/*吃东西*/}
}

// 保存为文件:Cat.js

然后是创建cat(加了一个 new 关键字):

var cats = []
for(var i=0; i<100; i++){
  cats.push(new Cat(i))
}

new 的作用,就是省那么几行代码。(也就是所谓的语法糖)

注意 constructor 属性

new 操作为了记录「临时对象是由哪个函数创建的」,所以预先给「Cat.prototype」加了一个 constructor 属性:

Cat.prototype = { constructor: Cat }

如果你重新对「Cat.prototype」赋值,那么这个 constructor 属性就没了,所以你应该这么写:

  Cat.prototype.run= function(){ /*走*/}
  Cat.prototype.eat = function(){ /*吃东西*/  }

或者你也可以自己给 constructor 重新赋值:

Cat.prototype = {
   constructor: Cat,
   run:function(){/*走*/},
   eat:function(){/*吃东西*/}
}

call apply bind

上面使用函数的时候采用call的方法去调用函数,后面跟的第一个参数就是指定的this对象,call和apply的区别是apply当接收多个参数的时候,以数组的形式去调用。call可以多个数去传递。前两者都是立即执行函数,而bind不会立即执行,而是去生成一个新的函数。新生成的函数,它的this就是bind传递进去的第一个参数。