这周来学习this,this虐我千百遍,我待this如初恋,每次一看this就会,就是指向它的调用者嘛,一做题就废,这次挖挖根,彻底了解一下this的机制
由来
为什么要用this
不使用this,需要显示的传递一个上下文对象
function identify(person){
return person.name
}
function speak(person){
var greeting = "Hello , I am " + identify(person)
console.log(greeting)
}
const p1={
name:"ABC"
}
const p2={
name:"DEF"
}
identify(p1) // ABC
speak(p2) // Hello , I am DEF
使用this,可以隐式传递对象引用,因此可以将API设计的更加简洁和易于复用
function identify(){
return this.name
}
const p1={
name:"ABC"
}
identify.call(p1) // ABC
this的误解
-
this指向自身(这是错误的!!!)
function fn(num){ console.log(num) this.count++ } fn.count = 0; for(var i=0;i<5;i++){ fn(i) } console.log(fn.count) //0====>?????按照this指向自身的说法,那么this.count++ 相当于 foo.count++ 实际输出的应该是5,但是最终输出的是0,显然这样理解是错误的。
解决方案
-
创建一个全局的count属性(利用了全局词法作用域,回避了this)
var count =0; function fn(){ this.count++ } for(var i=0;i<5;i++){ fn(i) } console.log(this.count) //5 -
创建一个函数count属性(利用了变量fn的词法作用域,回避了this)
function fn(){ fn.count++ } fn.count =0; for(var i=0;i<5;i++){ fn(i) } console.log(fn.count) //5 -
强制this指向fn
function fn(){ this.count++ } fn.count=0; for(var i=0;i<5;i++){ fn.call(fn,i) } console.log(fn.count) //5
-
-
this的作用域
this指向函数的作用域,这是错误的。在任何情况下this都不指向函数的词法作用域
function foo(){ var a = 2; this.bar() } function bar(){ console.log(a) } foo() //Uncaught ReferenceError: a is not defined词法作用域
window foo a: bar:function每当你想要把this和词法作用域的查找混合使用是,一定要提醒自己,这是无法实现的。
-
this到底是什么
this是在运行时进行绑定的,它的上下文取决于函数调用时的各种条件。
this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
this既不指向函数自身也不指向函数的词法作用域。
回顾一下JavaScript执行过程学习笔记,js在执行阶段(函数调用)进行this的绑定(即运行时绑定)
全貌解析
调用位置
调用位置:就是函数在代码中被调用的位置(不是函数声明的位置)
function baz(){
console.log('baz')
bar()
}
function bar(){
console.log('bar')
foo()
}
function foo(){
debugger
console.log('foo')
}
baz();
调用栈如图中Call Stack 此时this指向window
const obj = {
name:'测试',
baz:function(){
console.log('baz')
this.bar()
},
bar:function(){
console.log('bar')
this.foo()
},
foo:function(){
console.log(foo)
}
}
obj.baz()
调用栈如图中Call Stack 此时this指向obj
const person = {
name:'ddd'
}
function speak(){
console.log(this.name)
}
speak();// this 指向window
speak.call(person) // this 指向person
绑定规则
函数在执行过程中调用位置如何决定this的指向呢?
1.默认绑定
独立函数调用
-
非严格模式,this指向window
function foo(){ console.log(this.a) } var a = 2; foo()//2 -
严格模式,this 指向undefined
function foo(){ "use strict" console.error(this.a) } var a = 2; foo()//VM156:3 Uncaught TypeError: Cannot read property 'a' of undefined
2.隐式绑定
函数调用位置是否有上下文对象,或者说是否被某个对象包含或者拥有
function foo(){
console.log(this.a)
}
var a = 'window'
var obj ={
a:'obj',
foo:foo
}
obj.foo() //'obj'
调用位置使obj上下文来引用函数,隐式绑定规则把this指向当前的上下文对象obj
对象属性调用链中,只有上一次或者说最后一层在调用位置中起作用
function foo(){
console.log(this.a)
}
var a = 'window'
var obj1 ={
a:'obj1',
foo:foo
}
var obj2 ={
a:'obj2',
obj1:obj1,
foo:foo
}
obj2.obj1.foo() // 'obj1'
隐式丢失问题
-
赋值操作,bar = obj1.foo 实际是对foo函数的引用,bar调用时不含任何修饰符,隐藏应用默认绑定规则
function foo(){ console.log(this.a) } var a = 'window' var obj1 ={ a:'obj1', foo:foo } var bar = obj1.foo bar() // window -
函数作为参数传递,参数传递实际是一种隐式赋值,doFn(obj.foo) 实际 var fn = obj.foo; fn() ; fn调用时不含任何修饰符,隐藏应用默认绑定规则
function foo(){ console.log(this.a) } var a = 'window' var obj ={ a:'obj', foo:foo } function doFn(fn){ fn()// 调用位置 } doFn(obj.foo) // window -
回调函数this丢失
function foo(){ console.log(this.a) } var a = 'window' var obj ={ a:'obj', foo:foo } setTimeout(obj.foo,100) //'window'setTimeout 内部实现伪代码,函数调用时实际是把 obj.foo赋值给fn,fn实际是指向foo自身函数
function setTimeout(fn,delay){ // 延迟时间 fn() // 实际调用位置 }
3.显示绑定
回顾一下上面的隐式绑定,我们必须在一个对象内包含一个指向函数的属性,并且通过对象这个属性来间接引用来调用函数,从而把this隐式绑定到这个上下文对象上,那么如果我们不通过对象内部包含函数的方式,而想在对象上强制调用函数,那就需要显示绑定,可以通过函数的call()和apply()方法来实现。
call(obj,arg1,arg1,...),apply(obj,[arg1,arg2,..]),他们的第一个参数是对象,是给this准备,在函数调用的时候将其绑定到this,通过这种方式你可以直接指向this绑定的对象,称之为显示绑定
function foo(){
console.log(this.a)
}
var a = 'window'
var obj ={
a:'obj',
}
foo.call(obj)
显示绑定可以解决隐式丢失问题么?
function foo(){
console.log(this.a)
}
var a = 'window'
var obj ={
a:'obj',
}
foo.call(obj) // 'obj'
setTimeout(foo,100)// 'window'
可以看出,并不能解决隐式丢失问题,虽然foo.call(obj) 将 this指向了obj,但是显示绑定实在调用的时候绑定this,函数调用完之后this已经不再受你控制,但是无法保确定函数作为参数传递给其他函数时,其他函数会怎样调用这个函数。
举个例子,你用到的一个工具函数,这个函数是异步,需要你在异步结束之后做些操作,传统的操作的是通过回调函数,等工具函数的异步操作完成之后,调用你传递的函数,但是你自己的函数中用到了this,并且期望this指向特定的对象,使用call 或者 apply来进行显示绑定,显示绑定是在函数调用时进行绑定的,这个时候你的函数已经执行了,而期望的是工具函数异步执行完之后再调用自己的函数,而工具函数内如何调用自己的函数,我们是不知道的,可以直接调用,可以显示绑定this,此时this的指向已经不受我们控制。
function myFun(){
console.log(this.name)
}
var name = 'window'
var p1 = {
name:'p1'
}
var p2 = {
name:'p2'
}
myFun.call(p1); // 'p1'
// 工具函数
function util(fn){
//函数调用 对于使用者是黑盒操作 不知道内部如何调用,因此this不受控
fn()// 1、直接调用 'window'
fn.call(p2) 显示绑定的调用// 'p2'
}
util(myFun)
我们知道了是因为函数作为参数 或者回调传递是,内部是如何调用的我们是不清楚的,那也就是this的绑定是不受我们控制的,要解决这个问题就是在函数被传递的时候 进行this绑定同时让函数不发生调用,这样无论工具函数怎样调用你的函数,this指向都是确定的
function myFun(){
console.log(this.name)
}
var name = 'window'
var p1 = {
name:'p1'
}
var p2 = {
name:'p2'
}
// 工具函数
function util(fn){
setTimeout(fn,100)
}
// 包裹函数
function bindFn(obj){
return function(){
myFun.call(obj)
}
}
var fn = bindFn(p1) //返回的是函数,避免被直接调用
util(fn) // fn 执行的时候手动调用显示绑定,绑定this
创建一个函数包裹,在每次函数执行的时候将this绑定到预期的对象上,同时返回一个函数,避免函数被直接调用了,看到这里,有没有想起我们常用的bind函数?
bind源码:developer.mozilla.org/zh-CN/docs/…
if(!Function.prototype.mBind)(function(){
var slice = Array.prototype.slice
Function.prototype.mBind = function(){
var self = this;
var args = slice.call(arguments,1)
var target = arguments[0]
if(typeof self !=='function'){
throw new TypeError('Function.prototype.bind - ' +
'what is trying to be bound is not callable');
}
return function(){
return self.apply(target,args.concat(slice.call(arguments)))
}
}
})()
var person = {
name:'ddd',
say:function(){
console.log(this.name)
}
}
var say = person.say;
var b =say.mBind(person)
b()
4.new绑定
使用new来调用函数,或者说当函数发生函数调用的时候,会自动执行下面的操作
(1)创建一个全新的对象
(2)这个新对象执行[[Prototype]]连接
(3)新对象绑定到函数调用的this
(4)如果函数没有返回其他对象,那么new 表达式中的函数会自动返回这个新对象
看一下new操作的结果
function Person(name,age){
this.name = name ;
this.age = age;
}
var p = new Person('sss',122)
console.log(p)
自己实现:
function newOperator(ctor){
// 首先判断
if(typeof ctor !=='function'){
throw 'newOperator function the first param must be a function';
}
// 1.创建一个新的对象
var obj = {}
// 2.创建[[prototype]]连接
obj.__proto__ = ctor.prototype
// 3. 绑定this到新对象
var args = Array.prototype.slice.call(arguments, 1);
var result = ctor.apply(obj,args)
// 4. 判断返回值
var isObject = typeof result === 'object' && result !== null;
var isFunction = typeof result === 'function';
return isObject || isFunction ? result : obj;
}
function Person(name,age){
this.name = name ;
this.age = age;
}
var p1 = newOperator(Person,"ddd",'123')
console.log(p1)
1和2 步骤可以用es6的Object.create(ctor.prototype),创建一个新的对象并把进行[[prototype]]连接
优先级
this绑定有4种规则,那如果同时应用了多种,优先级又是怎样的呢?
默认绑定毋庸置疑一定优先级最低
隐式绑定和显示绑定
function say(){
console.log(this.name)
}
var p1 ={
name:'p1',
say:say
}
var p2 = {
name:'p2',
say:say
}
p1.say.call(p2);// p2
p1.say.call(p1)// p1
由此看书显示绑定的优先级高于隐式绑定
new绑定和隐式绑定
function foo(name){
this.name = name
}
var p1 = {
foo:foo
}
var p2 ={}
p1.foo('p1')
console.log(p1.name) //p1
p1.foo.call(p2,'p2')
console.log(p2.name) //p2
var p3 = new p1.foo('p3')
console.log(p1.name) //p1
console.log(p3.name) //p3
由此看书new绑定的优先级高于隐式绑定
总结
-
函数中是否存在new调用?如果是 this绑定的是新创建的对象
var p = new Person() -
函数通过 call、apply、bind调用?如果是this则指向绑定的对象
foo.call(p) -
函数是否在某个对象的上下文中调用(隐式调用)?如果是this则指向该上下文对象
var obj = { name:'dd', foo:foo } obj.foo() -
以上都不属于,则会默认绑定,严格模式this绑定到undefined,非严格模式则绑定到window
扩展
箭头函数
箭头函数不使用this的4条规则,而是根据外层函数或者全局作用域来决定this
call实现
Function.prototype.myCall = function(context){
var context = context ? context : window
context.fn = this
var args = Array.prototype.slice(arguments,1)
var result = context.fn(args)
delete context.fn
return result
}
apply实现
Function.prototype.myApply = function(context) {
context = context ? Object(context) : window
context.fn = this
let args = [...arguments][1]
if (!args) {
return context.fn()
}
let result = context.fn(args)
delete context.fn;
return result
}
练习
var obj = {
a: 10,
b: this.a + 10,
fn: function() {
return this.a;
}
}
console.log(obj.b); //NaN this.a this指向window this.a = undefined
console.log(obj.fn()); // 10 this指向obj 隐式绑定规则
var a = 20;
var obj = {
a: 10,
getA: function() {
return this.a;
}
}
console.log(obj.getA()); // 10 this指向obj 隐式绑定规则
var test = obj.getA;
console.log(test()); // 20 赋值之后 test中this 指向全局 默认绑定
var a = 5;
// 顶层函数:this指向window
function fn1() {
var a = 6;
console.log(a);
console.log(this.a); // this指向调用者
}
function fn2(fn) { // 此处相当于隐式赋值 fn = f1
var a = 7;
fn(); //window调用
}
var obj = {
a: 8,
getA: fn1
}
fn2(obj.getA); // 6 当前作用域中a=6; 5 this指向的是全局作用域window
function fn() {
// "use strict";
// 严格模式下:禁止this关键字指向全局对象
console.log(this); //undefined
var a = 1;
var obj = {
a: 10,
c: this.a + 20 //不能为undefined添加属性
}
return obj.c;
}
console.log(fn()); //Cannot read property 'a' of undefined
function Person(name, age) {
this.name = name;
this.age = age;
console.log(this);//使用new创建出来的对象,输出Person
}
Person.prototype.getName = function() {
console.log(this); //使用哪个对象来调用,this就指代谁,输出Person
}
var p1 = new Person("test", 18)
p1.getName()
var obj = {
foo: "test",
fn: function() { //对象的方法内,this指向该对象
var mine = this;
console.log(this.foo); //"test"
console.log(mine.foo); //"test"
// 方法的内部函数的this指向window
(function(){
console.log(this.foo); //undefined
console.log(mine.foo); //"test" mine指向方法的局部变量mine,最终指向obj对象
})()
}
}
obj.fn();
function foo() {
console.log(this.a);
}
var a = 2;
var o = {
a: 3,
foo: foo
}
var p = {
a: 4,
}
o.foo(); // 3 this指向o
(p.foo = o.foo)(); 2 this 自调用函数指向全局
p.foo = o.foo;
p.foo(); 4 // this 指向p
function foo() {
console.log(this.a);
}
var obj1 = {
a: 3,
foo: foo
};
var obj2 = {
a: 5,
foo: foo
};
obj1.foo();//3 隐式绑定
obj2.foo();//5 隐式绑定
obj1.foo.call(obj2)//5 显示绑定
obj2.foo.call(obj1)//3 显示绑定