深入javascript计划四:深入浅出this

这篇我们来深入了解this,this关键字是javascript中最复杂的机制之一,它是一个很特别的关键字,被自动定义在所有函数的作用域中。

什么是this?

1.this是javascript中的关键字。它是对象自动生成的一个内部对象,只能在对象内部使用。随着函数使用场合不同,this的值会发生变化。

2.this指向什么?完全取决于什么地方以什么方式调用,而不是创建时(this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象)。

小知识:

var定义的变量是window/global对象的属性,是js语言特有的属性之一。

window是js中的全局对象,我们创建的变量实际上是给window添加属性,所以可以使用window.xxx来访问对象。

let和const声明的变量不会添加到window对象中。

var a = "an";
console.log(window.a);// "an"

let b = "an";
console.log(window.b);// "undefined"
console.log(b); // an

const c = "an";
console.log(window.c);// "undefined"
console.log(c); // an

箭头函数与普通函数

普通函数:指向函数的调用者。有个简便的方法就是看函数前面有没有点,如果有点,那么就指向点前面的那个值。

箭头函数:指向函数所在的作用域。 注意对象的{}以及 if(){}都不构成作用域。

箭头函数与普通函数比较

箭头函数:

1.没有this,需要通过查找作用域来确定this的值。

(如果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this)

// let a = 1 // testObj.c() 为查询为undefined 因为let定义的变量不会添加到window
var a = 1 // testObj.c() 为查询为 1
function test() {
    let testObj = {
        a: 11,
        b: function() {
            // console.log(this) // 指向testObj
            let obj = {
                a: 10,
                b: function() {
                    console.log(this.a) // 10
                },
                c: {
                    a: 20,
                    fn: function() {
                        console.log(this.a) // 20
                    },
                },
                d: {
                    a: 30,
                    fn:() => {
                        // console.log(this) // 指向testObj
                        console.log(this.a) // 11
                    }
                }
            }
            obj.b()
            obj.c.fn()
            obj.d.fn()
        },
        c: () => {
            // console.log(this) // 指向window
            console.log(this.a) // 1
        },
        d: function() {
            console.log(this.a)  // 11
        }
    }
    testObj.b()
    testObj.c()
    testObj.d()
}
test()

主要讲解箭头函数,普通函数下面会详解

讲解:

testObj.c(),因为使用了箭头函数,所以从作用域开始查找,往上一层就是全局作用域,查询全局作用域有没有a,然后输出1,如果没有就输出undefined。

obj.d.fn(),因为使用了箭头函数,所以从作用域开始查找,往上一层就是b()发现它不是箭头函数就使用它的作用域,b()的this指向本身,然后查询b()的this有没有a这个变量,然后输出11,如果没有就输出undefined。

2.没有 arguments

let obj = {
    a: 10,
    b: () => {
        console.log(arguments)
    }
}
obj.b("a", "b") // Uncaught ReferenceError: arguments is not defined 

箭头函数没有自己的 arguments 对象,这不一定是件坏事,因为箭头函数可以访问外围函数的 arguments 对象:

function test() {
    return () => arguments[0]
}

let fn = test(1);
console.log(fn()); // 1

3.不能通过new关键词调用

JavaScript 函数有两个内部方法:[[Call]] 和 [[Construct]]。

当通过 new 调用函数时,执行 [[Construct]] 方法,创建一个实例对象,然后再执行函数体,将 this 绑定到实例上。

当直接调用的时候,执行 [[Call]] 方法,直接执行函数体。

箭头函数并没有 [[Construct]] 方法,不能被用作构造函数,如果通过 new 的方式调用,会报错。

let foo = () => {}
let fn = new foo() // TypeError: foo is not a constructor

4.没有new.target

因为不能使用 new 调用,所以也没有 new.target 值。想了解更多new.target

5.没有原型

由于不能使用 new 调用箭头函数,所以也没有构建原型的需求,于是箭头函数也不存在 prototype 这个属性。

let foo = () => {}
console.log(foo.prototype) // undefined

6.没有super

连原型都没有,自然也不能通过 super 来访问原型的属性,所以箭头函数也是没有 super 的,不过跟 this、arguments、new.target 一样,这些值由外围最近一层非箭头函数决定。

总结

MDN 的介绍:

An arrow function expression has a shorter syntax than a function expression and does not have its own this, arguments, super, or new.target. These function expressions are best suited for non-method functions, and they cannot be used as constructors.

翻译过来就是:

箭头函数表达式的语法比函数表达式更短,并且不绑定自己的this,arguments,super或 new.target。这些函数表达式最适合用于非方法函数(non-method functions),并且它们不能用作构造函数。

什么是non-method functions 呢?

A method is a function which is a property of an object.

翻译过来

对象属性中的函数就被称之为 method,那么 non-mehtod 就是指不被用作对象属性中的函数了。

可是为什么说箭头函数更适合 non-method 呢?看上面解释对比例子。

let obj = {
    i: 10,
    b: () => console.log(this.i, this),
    c: function() {
      console.log( this.i, this);
    }
}
obj.b(); // undefined {}-Window
obj.c(); // 10 { i: 10, b: [Function: b], c: [Function: c] }

 

普通函数this绑定规则

有三种绑定规则:默认绑定、隐式绑定、显示绑定

1.this默认绑定

普通函数:

window.a = 1; || var a = 1;
function foo() {
    console.log(this.a); // 1
}
foo();

这种就是默认绑定。看到 foo() 这种直接使用而不带任何修饰的函数调用 ,就默认且只能应用默认绑定。

默认绑定到window对象。

2.隐式绑定

普通函数:

window.a = 1; || var a = 1;
function foo(){
    // console.log(this);
    console.log(this.a);
}
var obj = {
    a : 10,
    foo : foo
}
foo(); // 1
obj.foo(); // 10

讲解:foo()直接调用就是我们上面所说的 默认绑定,this绑定到window上。

讲解:这里的this指向的是对象obj,因为你调用这个foo是通过obj.foo()执行的,那自然指向就是对象obj,这里再次强调一点,this的指向在函数创建的时候是决定不了的,在调用的时候才能决定,谁调用的就指向谁。

问题来了,看例子:

window.a = 1; || var a = 1;
function foo(){
    // console.log(this);
    console.log(this.a);
}
var obj = {
    a : 10,
    foo : foo
}
window.foo(); // 1
window.obj.foo(); // 10

这段代码和上面的那段代码几乎是一样的,但是这里的this为什么不是指向window,如果按照上面的理论,最终this指向的是调用它的对象。

这里先不解释为什么上面的那段代码this为什么没有指向window,我们再来看一段代码。

window.a = 1; || var a = 1;
function foo() {
    console.log(this.a)
}
var obj = {
    a : 10,
    b: {
        a: 20,
        foo: foo
    }
}
foo(); // 1
obj.b.foo(); // 20

这里同样也是对象obj调用,但是同样this并没有执行它,那你肯定会说我一开始说的那些不就都是错误的吗?

其实也不是,只是一开始说的不准确,接下来我将补充一句话,我相信你就可以彻底的理解this的指向的问题。

情况1:如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window,这里需要说明的是在js的严格版中this指向的不是window(这里不讨论严格模式)。

情况2:如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。

情况3:如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象。

window.a = 1; || var a = 1;
function foo() {
    console.log(this.a)
}
var obj = {
    a : 10,
    b: {
        // a: 20,
        foo: foo
    }
}
foo(); // 1
obj.b.foo(); // undefined

尽管对象b中没有属性a,这个this指向的也是对象b(因为this只会指向它的上一级对象,不管这个对象中有没有this要的东西)。

特殊一点的例子:

window.a = 1; || var a = 1;
function foo() {
    console.log(this.a)
}
var obj = {
    a : 10,
    b: {
        a: 20,
        foo: foo
    }
}
foo(); // 1
let f = obj.b.foo;
f(); // 1

这里this指向的是window,是不是有些蒙了?其实是因为你没有理解一句话,这句话同样至关重要。

this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的。

3.显示绑定

在我们刚刚的隐式绑定中有一个致命的限制,就是上下文必须包含我们的函数 ,例:

function foo() {}

let obj = {
   foo: foo
}

如果上下文不包含我们的函数用隐性绑定明显是要出错的,不可能每个对象都要加这个函数,那样的话扩展,维护性太差了,我们接下来聊的就是直接 给函数强制性绑定this。

3-1.call、apply

这里我们就要用到 js 给我们提供的函数 call 和 apply,它们的作用都是改变函数的this指向,第一个参数都是 设置this对象。

两个函数的区别:

  1. 除了第一个参数外,call 可以接收一个参数列表。
  2. 除了第一个参数外,apply只接受一个参数数组。

例子:

let a = {
    value: 1
}
function getValue(name, age) {
    console.log(name, age, this.value)
}
getValue.call(a, 'an', '21') // an 21 1
getValue.apply(a, ['an', '21']) // an 21 1

手写实现call、apply

可以从以下几点来考虑如何实现

1.不传入第一个参数,那么默认为window

2.改变了this指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?

call实现:

// 往Function原型链添加对象
Function.prototype.myCall = function(conext) {
    var context = conext || window;

    // 给context添加一个属性
    // getValue.myCall(a, 'an', '21') => a.fn = getValue
    context.fn = this;

    // 将 context 后面的参数取出来
    var args = [];
    var len = arguments.length;
    for(var i = 1; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    // 简易写法 var args = [...arguments].slice(1)

    // getValue.myCall(a, 'an', '21') => a.fn('an', '21')
    var result = eval('conext.fn(' + args + ')');
    
    // 删除fn
    delete conext.fn;
    return result;
}

let a = {
    value: 1
};
function getValue(name, age) {
    console.log(name, age, this.value);
}

getValue.myCall(a, 'an', '21'); // an 21 1

getValue.call(a, 'an', '21'); // an 21 1
getValue.apply(a, ['an', '21']); // an 21 1

简易版本(ES6)

// 往Function原型链添加对象
Function.prototype.myCall = function(context, ...args) {
    var context = context || window;
    context.fn = this;

    // console.log(args); // [ 'an', '21' ]
    console.log(context);

    var result = eval('context.fn(...args)');

    delete context.fn;
    return result;
}


let a = {
    value: 1
};
function getValue(name, age) {
    console.log(name, age, this.value);
}

getValue.myCall(a, 'an', '21'); // an 21 1

getValue.call(a, 'an', '21'); // an 21 1
getValue.apply(a, ['an', '21']); // an 21 1

apply实现:

// 往Function原型链添加对象
Function.prototype.myApply = function(conext) {
    var context = conext || window;
    context.fn = this;

    var result;
    // 需要判断是否存储第二个参数
    // 如果存在,就将第二个参数展开
    if (arguments[1]) {
        result = context.fn(...arguments[1]);
    } else {
        result = context.fn();
    }

    delete conext.fn;
    return result;
}

let a = {
    value: 1
};
function getValue(name, age) {
    console.log(name, age, this.value);
}

getValue.myApply(a, ['an', '21']); // an 21 1

getValue.call(a, 'an', '21'); // an 21 1
getValue.apply(a, ['an', '21']); // an 21 1

简易版本(ES6)

// 往Function原型链添加对象
Function.prototype.myApply = function(context, args) {
    var context = context || window;
    context.fn = this;

    console.log(args); // [ 'an', '21' ]
    var result = eval('context.fn(...args)');

    delete context.fn;
    return result;
}


let a = {
    value: 1
};
function getValue(name, age) {
    console.log(name, age, this.value);
}

getValue.myApply(a, ['an', '21']); // an 21 1

getValue.call(a, 'an', '21'); // an 21 1
getValue.apply(a, ['an', '21']); // an 21 1

3-2.bind

例子:

let a = {
    value: 1
};
function getValue(name, age) {
    console.log(name, age, this.value);
}
var test = getValue.bind(a); // bind会返回一个函数
test("an", 21); // an 21 1

bind和其他两个方法(call、apply)作用也是一致的,只是该方法会返回一个函数。

对于普通函数,绑定this指向。

对于构造函数,要保证原函数的原型对象上的属性不能丢失。

bind实现:

Function.prototype.myBind = function(context) {
    // 异常处理
    if (typeof this !== "function") {
        throw new TypeError("Error");
    }

    // 保存this的值,它代表调用 bind 的函数
    var _this = this;
    var args = [...arguments].slice(1);

    // 返回一个函数
    return function F() {
        // 因为返回一个函数,我们可以new F(),所以需要判断(F的原型在this原型链中)
        if (this instanceof F) {
            return new _this(...args, ...arguments);
        }
        return _this.apply(context, args.concat(...arguments));
    };
};
let a = {
    value: 1
};
function getValue(name, age) {
    console.log(name, age, this.value);
}
var test = getValue.bind(a); // bind会返回一个函数
test("an", 21); // an 21 1

var test2 = getValue.myBind(a); // myBind会返回一个函数
test2("an", 21); // an 21 1

3-3.new绑定

什么是new

js的new和面向对象(OOP)语言的new的作用都是 创建一个新的对象,但是他们的机制完全不同。

OOP:

创建一个新对象要经过构造函数(constructor),new 类名()会先调用类中的构造函数。

js:

js中只要用new装饰的函数就是构造函数(constructor),准确来说是函数的构造调用(因为js中不存在所谓的构造函数,ES6除外)。

js在new()过程中做了什么?

1. 新生成一个对象
2. 链接到原型(把这个新对象的__proto__属性指向 原函数的prototype属性)
3. 绑定this
4.返回新对象

例子:

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

foo(); // window对象
console.log(window.a); // 10 默认绑定

var obj = new foo(); // foo{ a : 10 }  创建的新对象的默认名为函数名

// console.log(obj); // foo { a : 10 }
console.log(obj.a); // 10

使用new调用函数后,函数会 以自己的名字 命名 和 创建 一个新的对象,并返回

特别注意 : 如果原函数返回一个对象类型,那么将无法返回新对象,你将丢失绑定this的新对象,例子:

function foo(){
    this.a = 10;
    return new String("an");
}
var obj = new foo();
console.log(obj.a); // undefined
console.log(obj); // "an"

看手写new例子,会很直观的认识它做的4步操作

function myNew() {
    // 创建一个空的对象
    let obj = new Object()
    // 获得构造函数
    let Con = [].shift.call(arguments)
    // 链接到原型
    obj.__proto__ = Con.prototype
    // 绑定 this,执行构造函数
    let result = Con.apply(obj, arguments)
    // 确保 new 出来的是个对象
    return typeof result === 'object' ? result : obj
}

对于实例对象来说,都是通过new产生的,无论是function foo() 还是 let a = {b: 1}。

对于创建一个对象来说,更推荐使用字面量的方式创建对象(无论性能上还是可读性)。因为你使用new Object的方式创建对象需要通过作用域链一层一层找到Object,但是你使用字面量方式就没有这个问题。

function foo() {}
// function 就是个语法糖
// 内部等同于 new Function()

let a = { b: 1 }
// 这个字面量内部也是使用了 new Object()

对new来说,还需要注意下运算符优先级

function foo() {
    return this;
}
foo.getName = function () {
    console.log('1');
};
foo.prototype.getName = function () {
    console.log('2');
};

new foo.getName();   // -> 1
new foo().getName(); // -> 2

从上图可以看出 new foo() 的优先级大于 new foo,所以对于上述代码来说可以这样划分执行顺序

function foo() {
    return this;
}
foo.getName = function () {
    console.log('1');
};
foo.prototype.getName = function () {
    console.log('2');
};

new (foo.getName()); // 1

// 分割线

function foo() {
    return this;
}
foo.getName = function () {
    console.log('1');
};
foo.prototype.getName = function () {
    console.log('2');
};

(new foo()).getName(); // 2

对于第一个函数来说,先执行了 foo.getName() ,所以结果为 1

对于后者来说,先执行 new foo() 产生了一个实例,然后通过原型链找到了 foo 上的 getName 函数,所以结果为 2。 

4.this绑定优先级

new绑定->显示绑定->隐式绑定->默认绑定

参考:

《你不知道的javascript》上卷

https://segmentfault.com/a/1190000011194676

https://www.cnblogs.com/pssp/p/5216085.html

https://juejin.im/post/5b14d0b4f265da6e60393680

https://yuchengkai.cn/docs/frontend/#new

http://47.98.159.95/my_blog/js-api/003.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

An_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值