this、call、apply、bind、new

236 阅读5分钟

this

ECMAScript规范: this关键字执行为当前执行环境的ThisBinding。
MDN:在绝大数情况下,函数的调用方式决定了this的值。
可以理解为,在JS中,this的指向是调用是决定的,而不是创建时决定的,这就导致this的指向会让人迷惑,简单来说,this具有运行期绑定的特性。

调用位置

调用位置就是函数在代码中被调用的位置,而不是声明的位置。
通过分析调用栈(到达当前执行位置所调用的所有函数)可以找到调用位置。

function baz() {
    console.log('baz');
    bar();
}
function bar() {
    console.log('bar');
    foo();
}
function foo() {
    console.log('foo');
}
baz();

当我们调用baz时,他会依次调用baz() --> bar() --> foo()
对于foo:调用位置是在bar函数中。
对于bar:调用位置是在baz函数中。
对于baz:调用位置是在全局作用域。

全局上下文

在全局执行上下文中this都指向全局对象(非严格模式)。

  • this等价于window对象
  • var === this. === window
console.log(window === this); // true
var a = 1;
this.b = 2;
window.c = 3;
console.log(a + b + c); // 6

function sum() {
    return this.a + this.b + this.c;
}
console.log(sum()); // 6

在非严格模式下,全局执行上下文中的thiswindow指向同一个内存对象。

函数上下文

在函数内部,this的取决于函数被调用的方式

直接调用

非严格模式下,在全局环境下直接调用函数,this指向window.

function foo() {
    return this;
}
console.log(foo() === window); // true

在严格模式下,在全局环境下直接调用函数,thisundefiend.

"use strict"
function foo() {
    return this;
}
const res = foo();
console.log(res);
console.log(res === window); // false

箭头函数

所有的箭头函数没有自己的this,箭头函数的this指向和外层的相同

function Person(name) {
    this.name = name;
    this.say = () => {
        var name = 'bj';
        return this.name;
    }
}
var person = new Person('bajiu');
console.log(person.say()); // bajiu

var a = 1;
function foo() {
    setTimeout(() => {
        console.log(this.a);
    }, 1000)
}
foo(); // 1
foo.call({a: 2}); // 2

对象的属性

this指向调用函数的对象

var name = 'bj'
var person = {
    name: 'bajiu',
    getName: function() {
        return this.name;
    }
};
person.getName(); // 函数返回:bajiu
const getName = person.getName;
getName(); // 函数返回:bj

作为构造函数

this 被绑定到正在构建的新对象

new执行大致步骤:

  1. 创建一个空对象
  2. 获取构造函数
  3. 设置空对象的原型
  4. 绑定this并执行构造函数
  5. 确保返回值为对象,null不算对象
function Person(name) {
    this.name = name;
    this.age = 25;
    this.say = function() {
        console.log(`${this.name}: ${this.age}` );
    }
}
var person = new Person('bajiu');
console.log(person.name); // bajiu
person.say(); // bajiu: 25

作为一个DOM事件处理函数

this指向出发事件的元素,也就是始事件处理锁绑定到DOM节点。

var ele = document.getElementById('id');
ele.addEventListener('click', function(e) {
   console.log(this);
   console.log(this === e.target); // true
});

HTML标签内联事件处理函数

this指向所在的DOM元素

<button onclick="console.log(this)">click me</button>

call、apply、bind

三个函数作用大体一致,callapply的区别在于后面参数的形式(如下),bind则是返回了一个新的函数,这个新函数的this指向thisArg。

fun.call(thisArg, arg1. arg2, ……)
fun.apply(thisArg, [arg1, arg2, ……]);
var newFoo = foo.bind(thisArg)

thisArg
fun函数运行时指定this值。需要注意的时,指定的this值并不定时该函数执行时真正的this

  • 非严格模式下,第一个参数如果为nullundefined时,this值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字、字符串、布尔值、symbol)的this会指向该原始值自动包装的对象。
  • 严格模式下,this传入值的。
// 非严格模式
var foo = function() {
    console.log(this);
}
foo.call(null); // window
foo.call(undefined); // window
foo.call(1); // Number{1};


// 严格模式
'use strict'
var bar = function() {
    console.log(this);
}
bar.call(null); // null
bar.call(undefined); // window
bar.call(1); // 1

call

call用于改变函数里面的this的指向,接受两个参数,第一个参数this的指向,可选参数,默认window。第二个参数调用函数需要的参数(数组形式)

Function.prototype.maCall = function(context) {
    if(typeof this !== 'function') {
        return;
    }
    const obj = context || window;
    obj.fn = this;
    cont params = [...arguments].slice(1);
    cont result = obj.fn(...params);
    delete obj.fn;
    return result;
}

apply

apply用于改变函数里里面this的指向,接受多个参数,第一个参数为this的指向,可选参数,默认window。后面参数为调用函数需要的参数

Function.prototype.myApply = function(context) {
    if(typeof this !== 'function') {
        return;
    }
    const obj = context || window;
    obj.fn = this;
    const params = arguments[1] || [];
    const res = obj.fn(...params);
    delete obj.fn;
    return res;
}

bind

this将永久的绑定到指定对象上(bind的第一个参数) 。bind改变this的指向,返回改变指向的函数,且只能改变一次。

  1. bindFunction原型链中Function.prototype的一个属性,每个函数都可以调用它。
  2. bind本身也是一个函数,返回值也是函数,函数的名称为bound 函数名
function foo (a, b) {
    console.log(this);
    console.log(a, b);
    return false;
}
let obj = {};
console.log(foo.bind(obj, 1, 2).name); // bound foo


var person = {
    name: 'bajiu',
    age: 25
};
function say(job) {
    console.log(`${this.name}: ${this.age} ${job}`);
}
var f = say.bind(person);
console.log(f('FE')); // bajiu: 25 FE

手写bind

Function.prototype.myBind = function(context, ...arg) {
    if(typeof this !== 'function') {
        return;
    }
    const fn = this;
    return function F(...arg2) {
        if(this instanceof F) {
            return new fn(...arg, ...arg2);
        }
        return fn.call(context, [...arg, ...arg2])
    }
}

升级版手写bind

Function.prototype.bind = function(context, ...params) {
    if(typeof this !== 'function') {
        throw new Error('this is must function');
    }
    const _this = this;
    var bound = function(...args) {
        const finalArg = params.concat(args);
        if(this instanceof bound) {
            if(_this.prototype) {
                function Empty(){};
                Empty.prototype = _this.prototype;
                bound.prototype = new Empty();
            }
            var result = _this.apply(this, finalArg);
            const type = typeof result;
            return result !== null &&  type === 'object'
                || type === 'function' ? result || this;
        }
        else {
            return _this.apply(context, finalArg);
        }
    }
    return bound;
}

new

new的实现过程大致分为五步

  1. 创建一个空对象
  2. 获取构造函数
  3. 设置空对象的原型
  4. 绑定this并执行构造函数
  5. 确保返回值为对象

手写new

function create(f, ...params) {
    const obj = {};
    obj.__proto__ = f.prototype;
    const result = f.apply(obj, params);
    return typeof result === 'object' ? result : obj;
}

思考题

奇怪的冷知识

var person = {
    name: 'bajiu',
    age: 25,
    getName: () => {
        console.log(this.name);
    }
}
person.getName(); // 输出 空字符串
person.getName.call({name: 'bj'}); // 输出 空字符串

对this理解比较清楚的同学肯定会回答,最后输出都是undefiend
首先明确箭头函数无其自身的this,它的this指向与它上级的this指向相同,那此时this指向window, 此时就涉及到了一个冷知识了,window对象上默认是有name属性的,且值默认为''

资料

  1. github.com/axuebin/art…
  2. github.com/mqyqingfeng…
  3. juejin.cn/post/684490…
  4. 面试官问:能否模拟实现JS的call和apply方法
  5. 面试官问:能否模拟实现JS的bind方法