17

深入浅出this的理解


问题的由来


var obj = {
    foo: function(){}
}

var foo = obj.foo;

// 写法一
obj.foo();

// 写法二
foo();

虽然obj.foo和foo指向同一个函数,但是执行结果可能不一样。

var obj = {
    foo: function() {
        conosle.log(this.bar)
    },
    bar: 2
};

var foo = obj.foo;
var bar = 3;

obj.foo(); // 2
foo(); // 3

这种差异的原因就是因为内部使用了this关键字,this指向的是函数运行的所在环境,对于obj.foo()来说,this执行obj,对于foo()来说,this指向window全局环境

this的原理


内存的数据结构

JavaScript 语言之所以有this的设计,跟内存里面的数据结构有关系。

var obj = {foo: 5}

clipboard.png

也就是或变量obj是一个地址,后面读取obj.foo引擎先从obj拿到地址,然后再从该地址读取原始对象,返回它的属性值。

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的。

clipboard.png


函数


这样的结构是很清晰的,问题在于属性的值可能是一个函数。

var obj = { foo: function () {} };

这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。

clipboard.png

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。

var f = function () {};
var obj = { f: f };

// 单独执行
f()

// obj 环境执行
obj.f()

环境变量


var f = function () {
  console.log(x);
};

上面代码中,函数体里面使用了变量x。该变量由运行环境提供。

现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

// obj 环境执行
obj.f() // 2

在obj环境执行,this.x指向obj.x。
函数f在全局环境执行,this.x指向全局环境的x。

回到我们最初的问题 obj.foo()是通过obj找到foo,所以就是在obj环境执行。一旦var foo = obj.foo,变量foo就直接指向函数本身,所以foo()就变成在全局环境执行。

阮一峰老师的 this原理


继续我们的this

this在js中一直是谜一样的存在着,在面试中也是经常会被问道

this的指向在函数创建的时候是决定不了的,在调用的时候才能决定

  • 全局范围内
this;    //在全局范围内使用`this`,它将会指向全局对象

var name="zhoulujun";

function say(){
    console.log(this.name)
}
say(); //zhoulujun

当执行 say函数的时候, JavaScript 会创建一个 Execute context (执行上下文),执行上下文中就包含了 say函数运行期所需要的所有信息。 Execute context 也有自己的 Scope chain, 当函数运行时, JavaScript 引擎会首先从用 say函数的作用域链来初始化执行上下文的作用域链。

  • 函数调用
foo();    //this指向全局对象
  • 方法调用*
test.foo();    //this指向test对象
  • 调用构造函数*
new foo();    //函数与new一块使用即构造函数,this指向新创建的对象
  • 显式的设置this*
function foo(a, b, c) {}
var bar = {};
foo.apply(bar, [1, 2, 3]);    //this被设置成bar
foo.call(bar, 1, 2, 3);       //this被设置成bar

从函数调用理解this

实例1

myObj3={
        site:"zhoulujun.cn",
        andy:{
            site:"www.zhoulujun.cn",
            fn:function(){
                console.log(this)
                console.log(this.site)
            }
        }
    };
var site="111";
var fn=myObj3.andy.fn;
fn();  // 这里的调用环境是window


// Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
// 111

clipboard.png

实例2

  • 如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象
myObj3={
    site:"zhoulujun.cn",
    andy:{
        site:"www.zhoulujun.cn",
        fn:function(){
            console.log(this)
            console.log(this.site)
        }
    }
};
var site="111";
myObj3.andy.fn();


VM51:6 {site: "www.zhoulujun.cn", fn: ƒ}
VM51:7 www.zhoulujun.cn

clipboard.png

实例3

document.getElementById( 'div1' ).onclick = function(){
    console.log( this.id );// 输出: div1
    var func = function(){ 
        console.log ( this.id );// 输出: undefined
    } 
    func();
}; 
//修正后
document.getElementById( 'div1' ).onclick = function(){
    var func = function(){ 
        console.log ( this.id );// 输出: div1
    } 
    func.call(this);
}; 

实例4

var A = function( name ){ 
    this.name = name;
};
var B = function(){ 
    A.apply(this,arguments);
};
B.prototype.getName = function(){ 
    return this.name;
};
var b=new B('sven');
console.log( b.getName() ); // 输出:  'sven'

实例5

function foo() {
    console.log( this.a );
}

var obj1 = {
    a: 2,
    foo: foo
};

var obj2 = {
    a: 3,
    foo: foo
};

obj1.foo(); // 2
obj2.foo(); // 3

obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

apply、call

因为apply、call存在于Function.prototype中,所以每个方法都有这两个属性。

call
函数名.call(对象,arg1....argn)
//功能:
    //1.调用函数
    //2.将函数内部的this指向第一个参数的对象
    //3.将第二个及以后所有的参数,作为实参传递给函数
apply主要用途是直接用数组传参
函数名.apply(对象, 数组/伪数组);
//功能:
    //1.调用函数
    //2.将函数内部的this指向第一个参数的对象
    //3.将第二个参数中的数组(伪数组)中的元素,拆解开依次的传递给函数作为实参
//案例求数组中最大值
var a=Math.max.apply( null, [ 1, 2, 5, 3, 4 ] );
console.log(a);// 输出:5

call应用(将伪数组转为数组)

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
Array.prototype.join.call(arrayLike, '&'); // name&age&sex
Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"] 
// slice可以做到类数组转数组
Array.prototype.map.call(arrayLike, function(item){
    return item.toUpperCase();
}); 
// ["NAME", "AGE", "SEX"]


console.log(
    Object.prototype.toString.call(num),
    Object.prototype.toString.call(str),
    Object.prototype.toString.call(bool),
    Object.prototype.toString.call(arr),
    Object.prototype.toString.call(json),
    Object.prototype.toString.call(func),
    Object.prototype.toString.call(und),
    Object.prototype.toString.call(nul),
    Object.prototype.toString.call(date),
    Object.prototype.toString.call(reg),
    Object.prototype.toString.call(error)
);
// '[object Number]' '[object String]' '[object Boolean]' '[object Array]' '[object Object]'
// '[object Function]' '[object Undefined]' '[object Null]' '[object Date]' '[object RegExp]' '[object Error]'

Meils
1.6k 声望157 粉丝

前端开发实践者