当前位置 博文首页 > _Fatman:JavaScript——深入了解this

    _Fatman:JavaScript——深入了解this

    作者:_Fatman 时间:2021-01-26 17:09

    前言

    我曾以为func()其实就是window.func()

    function func(){
    	console.log('this : ' + this);
    }
    
    func();//this : [object Window]
    window.func();//this : [object Window]
    

    直到

    'use strict'
    function func(){
    	console.log('this : ' + this);
    }
    func();//this : undefined
    window.func();//this : [object Window]
    

    也曾为输出inside this : [object Window] 而困惑不已

    function outside(){
    	console.log('outside  this : ' + this);//outside  this : [object Object]
    	function inside(){
    		console.log('inside  this : ' + this);//inside  this : [object Window]
    	}
    	inside();
    }
    let obj = {
    	outside : outside
    }
    obj.outside();
    

    曾感慨Java之美好[1],唾弃JavaScript中this的‘灵活’。
    ...

    一直到我尝试总结出this的规律:
    1.构造函数中的this关键字在任何模式下都指向new出来的对象;

    2.严格模式下this关键字指向调用该函数的对象,如果该函数未被对象调用,则 this === 'undefined';

    3.非严格模式下this关键字指向调用该函数的对象,如果该函数未被对象调用 this === window;

    再到后来,拜读了JavaScript语言精粹,知晓了4种调用模式,知晓了箭头函数的一个我不曾知晓的作用,结合过往,我感觉自己已经摸清了this的规律,亦或者至少摸清了一部分的规律,特撰此文,作为总结;

    首先,此文所有代码运行环境皆为浏览器,所以我不会强调global;
    再者,function中this指向被确定于function被调用时(抛开箭头函数和class不论),
    类似像下面这种代码,我觉得没有提的必要;

    function func(){
    	console.log('this : ' + this);
    }
    func;
    setTimeout(func,100); 
    //我想表达的意思是 func是被setTimeout调用的 同样我也可以写一个mySetTimeout
    mySetTimeout = function(func,delay){
    	setTimeout(func.bind({}),delay); //我们应该把目光放在function调用时
    }
    mySetTimeout(func,100); 
    //在不运行代码的前提下,如果不看mySetTimeout代码,能准确判断this是什么吗?
    

    最后,我不想提with,因为with的使用往往会引起歧义,就如同下面的代码,明明调用时的代码一模一样,但一个在全局作用域window中调用func,而另一个在obj的作用域中调用,输出的结果天差地别。

    function func(){
    	console.log('this : ' + this);
    }
    let obj = {};
    with(obj){
    	func();//this : [object Window]
    }
    obj.func = func;
    with(obj){
    	func();//this : [object Object]
    }
    

    接下来的内容我将以下图中的思路展开:
    在这里插入图片描述

    ES6之前

    这里的主要思路还是沿用的JavaScript语言精粹。

    函数调用模式

    JavaScript中的function不同于Java,Java虽然说万物皆对象,但是基础类型和function就不是对象。Java中的function只是对象的行为,但是JavaScript不同,JavaScript虽然同时包含了一些像原型、函数柯里化等编程思想,但是在万物皆对象这一方面,反而比Java更像是面向对象编程。JavaScript中的function是支持直接调用的。
    在非严格模式

    function func(){
    	console.log('this : ' + this);
    }
    func();//this : [object Window]
    

    在严格模式

    'use strict'
    function func(){
    	console.log('this : ' + this);
    }
    func();//this : undefined
    

    方法调用模式

    方法调用模式就是把function当成对象的行为来调用,既然是对象的行为,那么function中的this指向的当然是这个调用的对象了。
    在非严格模式

    let _self = null;
    function func(){
    	_self = this;
    }
    let obj = {
    	 func : func
    }
    obj.func();
    console.log('_self === obj : ' + (_self === obj));//_self === obj : true
    

    在严格模式

    'use strict'
    let _self = null;
    function func(){
    	_self = this;
    }
    let obj = {
    	 func : func
    }
    obj.func();
    console.log('_self === obj : ' + (_self === obj));//_self === obj : true
    

    构造调用模式

    构造调用模式就是把function当做构造函数调用,在其左边加上new关键字,为了迎合代码规范,这里的function我将以大写字母开头。
    在非严格模式

    let _self = null;
    function Person(){
    	_self = this;
    }
    let person = new Person();
    console.log('_self === person : ' + (_self === person));//_self === person : true
    

    在严格模式

    'use strict'
    let _self = null;
    function Person(){
    	_self = this;
    }
    let person = new Person();
    console.log('_self === person : ' + (_self === person));//_self === person : true
    

    构造函数这里我觉得有必要扩展一下:
    1.构造函数中返回对象(非基础类型),会影响上面的结果;

    let _self = null;
    function Person(){
    	_self = this;
    	return window;
    }
    let person = new Person();
    console.log('_self === person : ' + (_self === person));//_self === person : false
    console.log('window === person : ' + (window === person));//window === person : true
    

    2.省略new关键字,同样会影响上面的结果;

    let _self = null;
    function Person(){
    	_self = this;
    }
    let person = Person();
    console.log('_self === person : ' + (_self === person));//_self === person : false
    console.log('window === person : ' + (window === person));//window === person : false
    console.log('typeof person : ' + typeof person);//typeof person : undefined
    

    在Person调用时省略new关键字还可能会污染全局作用域

    function Person(){
    	this.personName = 'person';
    }
    let person = Person();
    console.log('person.personName : '+person.personName);//Cannot read property 'personName' of undefined
    console.log('window.personName : '+window.personName);//window.personName : person
    

    蠢办法解决调用构造函数不用new关键字的:

    function Person(){
    	if(this === window){
            throw Error('You must use the new keyword.');
        }
    	this.personName = 'person';
    }
    let person = Person();//You must use the new keyword.
    

    改进版

    function Person(){
    	let context;
        (function(){
            context = this;
        }())
        if(this === context){
            throw Error('You must use the new keyword.');
        }
    	this.personName = 'person';
    }
    let person = Person();//You must use the new keyword.
    

    特指调用模式

    bind虽然是es6的,但是我也放到这个模式一起讲了,因为我觉得把bind和apply、call一起讲可能会更容易理解一些。

    apply

    apply的第一个参数是绑定的对象,第二个参数是array。call和apply的不同之处在于call的第二个参数对于function中arguments的第一位,第三个参数对于function中的arguments的第二位,以此类推;而apply的第二个参数对应function中的arguments。由于这里主要是讲this,所以第二个参数的例子就不提了,后面的call也一样。
    在非严格模式

    function func(){
    	console.log('this : ' + this);
    }
    func.apply({});//this : [object Object]
    func.apply(window);//this : [object Window]
    func.apply(null);//this : [object Window]
    func.apply();//this : [object Window]
    

    在严格模式

    'use strict'
    function func(){
    	console.log('this : ' + this);
    }
    func.apply({});//this : [object Object]
    func.apply(window);//this : [object Window]
    func.apply(null);//this : null
    func.apply();//this : undefined
    

    实现apply

    满足条件

    1.把第一个参数绑定到调用myApply的function运行时的this;
    2.第二个参数应与调用myApply的function的arguments内容一致;
    3.严格模式和非严格模式第一个参数为null或undefined时情况要与apply函数一致;

    代码
    Function.prototype.myApply = function(){
    	var context,arr;
    	//谁调用的myApply this就指向谁
    	if(typeof this !== 'function'){
    		throw Error('typeof this !== "function"');
    	}
    	context = arguments[0];
    	arr = arguments[1] ? arguments[1] : [];
    	if(typeof context === 'undefined' || context === null){
    		//满足条件3
    		context = (function(){
    			return this;
    		}());
    	}
    	if(typeof context === 'undefined'){
    		this(...arr);
    	}else{
    		context.f = this;
    		context.f(...arr);
    	}
    }
    

    call

    call如果只传入第一个参数,结果和只传入第一个参数的apply是一致的。
    在非严格模式

    function func(){
    	console.log('this : ' + this);
    }
    func.call({});//this : [object Object]
    func.call(window);//this : [object Window]
    func.call(null);//this : [object Window]
    func.call();//this : [object Window]
    

    在严格模式

    'use strict'
    function func(){
    	console.log('this : ' + this);
    }
    func.call({});//this : [object Object]
    func.call(window);//this : [object Window]
    func.call(null);//this : null
    func.call();//this : undefined
    

    实现call

    满足条件

    1.把第一个参数绑定到调用myCall的function运行时的this;
    2.除第一个参数外其余参数组成的数组应与调用myCall的function的arguments内容一致;
    3.严格模式和非严格模式第一个参数为null或undefined时情况要与call函数一致;

    代码
    Function.prototype.myCall = function(){
    	var context,arr;
    	//谁调用的myCall this就指向谁
    	if(typeof this !== 'function'){
    		throw Error('typeof this !== "function"');
    	}
    	context = arguments[0];
    	//差异点 call与apply的传值方式所致
    	arr = [...arguments].slice(1);//手动转型
    	if(typeof context === 'undefined' || context === null){
    		//满足条件3
    		context = (function(){
    			return this;
    		}());
    	}
    	if(typeof context === 'undefined'){
    		this(...arr);
    	}else{
    		context.f = this;
    		context.f(...arr);
    	}
    }
    

    bind

    bind和call很相似,主要的不同点在于func.call(window)立马就调用了,而func.bind(window)会返回一个绑定了window的function,但是这个function还没有执行。可以这样理解func.bind(window)()的效果与func.call(window)一致。
    在非严格模式

    function func(){
    	console.log('this : ' + this);
    }
    
    func.bind({})();//this : [object Object]
    func.bind(null)();//this : [object Window]
    func.bind()();//this : [object Window]
    let obj = {
    	func : func.bind(window)
    }
    obj.func();//this : [object Window]
    //构造函数
    let _self = null;
    function Person(){
    	_self = this;
    }
    let P = Person.bind(window);
    let person = new P();
    console.log('_self === person : ' + (_self === person));//_self === person : true
    console.log('window === person : ' + (window === person));//window === person : false
    

    在严格模式

    'use strict'
    function func(){
    	console.log('this : ' + this);
    }
    
    func.bind({})();//this : [object Object]
    func.bind(null)();//this : null
    func.bind()();//this : undefined
    let obj = {
    	func : func.bind(window)
    }
    obj.func();//this : [object Window]
    //构造函数
    let _self = null;
    function Person(){
    	_self = this;
    }
    let P = Person.bind(window);
    let person = new P();
    console.log('_self === person : ' + (_self === person));//_self === person : true
    console.log('window === person : ' + (window === person));//window === person : false
    

    从上面的例子,我们不单单可以发现bind在严格模式和非严格模式下的不同,还可以得出构造调用模式的优先级最高,bind其次,方法调用模式和函数调用模式最低。

    实现bind

    满足条件

    1.把第一个参数绑定到调用myBind的function运行时的this;
    2.将除第一个参数外其余参数与function中参数合并;
    3.严格模式和非严格模式第一个参数为null或undefined时情况要与bind函数一致;

    代码
    Function.prototype.myBind = function(){
    	var context,arr,_self;
    	//谁调用的myBind this就指向谁
    	if(typeof this !== 'function'){
    		throw Error('typeof this !== "function"');
    	}
    	context = arguments[0];
    	arr = [...arguments].slice(1);//手动转型
    	if(typeof context === 'undefined' || context === null){
    		//满足条件3
    		context = (function(){
    			return this;
    		}());
    	}
    	_self = this;
    	return function(){
    		if(typeof context === 'undefined'){//严格模式
    			_self(arr.concat(...arguments));
    		}else{
    			context.f = _self;
    			context.f(arr.concat(...arguments));
    		}
    		
    	}
    }
    

    ES6

    据我所知,有一部分人,他们奉行箭头函数+class来解决一切问题。我对此观点的正确性不表态,但是这样做能减少很多判断this的麻烦。

    箭头函数

    箭头函数没有this,箭头函数中的this来自于它处于的作用域链中的上一层。我在前言中说过,我曾为输出inside this : [object Window] 而困惑不已,但是我现在把代码略微修改一下,输出就将符合我的预期(inside继承了outside的this值)。

    function outside(){
    	console.log('outside  this : ' + this);//outside  this : [object Object]
    	let inside = ()=>{
    		console.log('inside  this : ' + this);//inside  this : [object Object]
    	}
    	inside();
    }
    let obj = {
    	outside : outside
    }
    obj.outside();
    

    要是把outside也改成箭头函数,结果又会大不一样

    let outside = ()=>{
    	console.log('outside  this : ' + this);//outside  this : [object Window]
    	let inside = ()=>{
    		console.log('inside  this : ' + this);//inside  this : [object Window]
    	}
    	inside();
    }
    let obj = {
    	outside : outside
    }
    obj.outside();
    

    因为箭头函数的this值是继承于它身处的作用域上一层的this,outside上一层是全局作用域,不会再发生更改了,所以这里就算用方法调用模式,也无法改变this的值。
    在非严格模式

    let func = ()=>{
    	console.log('this : ' + this);
    }
    func();//this : [object Window]
    let obj = {
    	func : func
    }
    obj.func();//this : [object Window]
    func.apply({});//this : [object Window]
    func.call({});//this : [object Window]
    func.bind({})();//this : [object Window]
    func.apply(null);//this : [object Window]
    func.apply();//this : [object Window]
    
    let _self = null;
    let Person = ()=>{
    	_self = this;
    }
    let person = new Person();//Person is not a constructor
    

    在严格模式

    'use strict'
    let func = ()=>{
    	console.log('this : ' + this);
    }
    func();//this : [object Window]
    let obj = {
    	func : func
    }
    obj.func();//this : [object Window]
    func.apply({});//this : [object Window]
    func.call({});//this : [object Window]
    func.bind({})();//this : [object Window]
    func.apply(null);//this : [object Window]
    func.apply();//this : [object Window]
    
    let _self = null;
    let Person = ()=>{
    	_self = this;
    }
    let person = new Person();//Person is not a constructor
    
    

    观察上述代码运行结果可知:
    1.严格模式和非严格模式对箭头函数中的this无影响;
    2.箭头函数无法当作构造函数使用;
    3.箭头函数中的this只与自身处于的作用域链上一层有关;

    class

    第一次看到class的用法时,我就不禁感慨原型的强大,对于我这种以前使用Java的人来说,class真的是太友好了。
    在非严格模式

    let constructorThis = null;
    let funcThis = null;
    let staticFuncThis = null;
    class Person{
    	constructor(){
    		constructorThis = this;
    	}
    	func(){
    		funcThis = this;
    	}
    	static staticFunc(){
    		staticFuncThis = this;
    	}
    }
    
    let person = new Person();
    person.func();
    Person.staticFunc();
    
    console.log('constructorThis === person : ' + (constructorThis === person));//constructorThis === person : true
    console.log('funcThis === person : ' + (funcThis === person));//funcThis === person : true
    console.log('staticFuncThis === person : ' + (staticFuncThis === person));//staticFuncThis === person : false
    console.log('staticFuncThis : ' + staticFuncThis);//staticFuncThis : class Person...
    

    在严格模式

    'use strict'
    let constructorThis = null;
    let funcThis = null;
    let staticFuncThis = null;
    class Person{
    	constructor(){
    		constructorThis = this;
    	}
    	func(){
    		funcThis = this;
    	}
    	static staticFunc(){
    		staticFuncThis = this;
    	}
    }
    
    let person = new Person();
    person.func();
    Person.staticFunc();
    
    console.log('constructorThis === person : ' + (constructorThis === person));//constructorThis === person : true
    console.log('funcThis === person : ' + (funcThis === person));//funcThis === person : true
    console.log('staticFuncThis === person : ' + (staticFuncThis === person));//staticFuncThis === person : false
    console.log('staticFuncThis : ' + staticFuncThis);//staticFuncThis : class Person...
    

    观察上述代码运行结果可知:
    1.严格模式和非严格模式对class function中的this无影响;
    2.构造函数和普通方法的this就是new出来的值(和方法调用模式、构造调用模式一致)
    3.静态方法的this就是这个class(还是和方法调用模式一致 毕竟是用class调用的静态方法)

    结尾

    由于本人水平有限,如有缺失和错误,还望告知。


    1. Java中function只能是方法,被对象或者类调用。非静态方法被对象调用时,this是这个调用的对象;静态方法被类调用时,则没有this; ??

    下一篇:没有了