阿里云主机折上折
  • 微信号
Current Site:Index > Function invocation methods and the this binding

Function invocation methods and the this binding

Author:Chuan Chen 阅读数:8204人阅读 分类: JavaScript

Function Invocation Methods and this Binding

In JavaScript, the way a function is called directly affects the binding rules of this. Different invocation scenarios can cause this to point to entirely different objects. Understanding these rules is crucial for writing reliable code.

Default Binding (Standalone Function Invocation)

When a function is called as a standalone function, this points to the global object (in browsers, window) in non-strict mode and undefined in strict mode.

function showThis() {
  console.log(this);
}

showThis(); // Outputs the `window` object in browsers

'use strict';
function strictShow() {
  console.log(this);
}
strictShow(); // Outputs `undefined`

This binding often occurs in callback functions:

setTimeout(function() {
  console.log(this); // Outputs `window` in browsers
}, 100);

Implicit Binding (Method Invocation)

When a function is called as an object method, this points to the object that invoked the method.

const user = {
  name: 'Alice',
  greet: function() {
    console.log(`Hello, ${this.name}!`);
  }
};

user.greet(); // Outputs "Hello, Alice!"

Nested object scenario:

const company = {
  name: 'TechCorp',
  department: {
    name: 'Dev',
    showName: function() {
      console.log(this.name);
    }
  }
};

company.department.showName(); // Outputs "Dev"

Implicit Binding Loss Issue

When a method is assigned to a variable or passed as an argument, implicit binding can be lost:

const counter = {
  count: 0,
  increment: function() {
    this.count++;
    console.log(this.count);
  }
};

const incrementFn = counter.increment;
incrementFn(); // Outputs NaN (`this` points to the global object)

// Typical issue in event handlers
document.getElementById('btn').addEventListener('click', counter.increment); 
// When clicked, `this` points to the DOM element, not the `counter` object

Explicit Binding (call/apply/bind)

Using call, apply, or bind allows you to forcibly specify the binding of this.

function introduce(lang) {
  console.log(`I'm ${this.name}, I code with ${lang}`);
}

const dev = { name: 'Bob' };

introduce.call(dev, 'JavaScript'); // Outputs "I'm Bob, I code with JavaScript"
introduce.apply(dev, ['Python']);  // Outputs "I'm Bob, I code with Python"

const boundFn = introduce.bind(dev);
boundFn('Java'); // Outputs "I'm Bob, I code with Java"

bind creates a new function with this permanently bound:

const boundIncrement = counter.increment.bind(counter);
document.getElementById('btn').addEventListener('click', boundIncrement);
// Now correctly increments `counter.count`

new Binding (Constructor Invocation)

When a function is called with new, this points to the newly created object instance.

function Person(name) {
  this.name = name;
  this.sayHi = function() {
    console.log(`Hi, I'm ${this.name}`);
  };
}

const alice = new Person('Alice');
alice.sayHi(); // Outputs "Hi, I'm Alice"

The this binding process inside a constructor:

  1. Creates a new object
  2. Sets the new object's prototype to the constructor's prototype
  3. Binds this to the new object
  4. Executes the constructor code
  5. If the constructor doesn't return an object, returns this

this in Arrow Functions

Arrow functions do not bind their own this; instead, they inherit the this value from the outer scope.

const timer = {
  seconds: 0,
  start: function() {
    setInterval(() => {
      this.seconds++;
      console.log(this.seconds);
    }, 1000);
  }
};

timer.start(); // Correctly increments `seconds` every second

Comparison with regular functions:

const obj = {
  value: 42,
  regularFn: function() {
    setTimeout(function() {
      console.log(this.value); // Outputs `undefined` (`this` points to the global object)
    }, 100);
  },
  arrowFn: function() {
    setTimeout(() => {
      console.log(this.value); // Outputs 42
    }, 100);
  }
};

Handling this in Callback Functions

Modern JavaScript provides multiple ways to handle this binding in callback functions:

// Method 1: Closure to save `this`
class Component {
  constructor() {
    this.value = 100;
    const self = this;
    button.onclick = function() {
      console.log(self.value);
    };
  }
}

// Method 2: Arrow function
class Component {
  constructor() {
    this.value = 200;
    button.onclick = () => {
      console.log(this.value);
    };
  }
}

// Method 3: `bind`
class Component {
  constructor() {
    this.value = 300;
    button.onclick = this.handleClick.bind(this);
  }
  
  handleClick() {
    console.log(this.value);
  }
}

this in DOM Event Handlers

In DOM event handlers, this by default points to the element that triggered the event:

document.querySelector('button').addEventListener('click', function() {
  console.log(this); // Outputs the `<button>` element
});

When using arrow functions, this does not point to the element:

document.querySelector('button').addEventListener('click', () => {
  console.log(this); // Outputs the `this` of the outer scope (usually `window`)
});

this Binding in Classes

Class methods require special attention to this binding, especially when passed as callbacks:

class Logger {
  constructor() {
    this.logs = [];
  }
  
  addLog(message) {
    this.logs.push(message);
    console.log(this.logs);
  }
}

const logger = new Logger();
document.getElementById('btn').addEventListener('click', logger.addLog); // Error
document.getElementById('btn').addEventListener('click', logger.addLog.bind(logger)); // Correct

Class field syntax can automatically bind this:

class Logger {
  logs = [];
  
  addLog = (message) => {
    this.logs.push(message);
    console.log(this.logs);
  }
}

Priority of this Binding

When multiple rules apply, the binding priority from highest to lowest is:

  1. new binding
  2. Explicit binding (call/apply/bind)
  3. Implicit binding (method invocation)
  4. Default binding
function test() {
  console.log(this.name);
}

const obj1 = { name: 'obj1', test };
const obj2 = { name: 'obj2', test };

obj1.test(); // obj1 (implicit binding)
obj1.test.call(obj2); // obj2 (explicit binding has higher priority)
new obj1.test(); // undefined (`new` binding has the highest priority)

this in Special Scenarios

Certain APIs allow specifying the this value for callback functions:

[1, 2, 3].forEach(function(item) {
  console.log(item, this); // `this` points to the second argument passed
}, { customThis: true });

fetch('/api').then(function() {
  console.log(this); // `undefined` in strict mode
});

this in module scope:

// In ES modules
console.log(this); // Outputs `undefined`

this in Immediately Invoked Function Expressions (IIFE)

this in IIFEs depends on the invocation method:

(function() {
  console.log(this); // `window` in non-strict mode, `undefined` in strict mode
})();

const obj = {
  method: function() {
    (function() {
      console.log(this); // `window` in non-strict mode, not `obj`
    })();
  }
};

Functions as Getters/Setters

When a function is called as an object getter or setter, this points to the object:

const account = {
  balance: 1000,
  get formattedBalance() {
    return `$${this.balance}`;
  },
  set setBalance(value) {
    this.balance = value;
  }
};

console.log(account.formattedBalance); // Outputs "$1000"
account.setBalance = 2000;

this in Prototype Chains

For methods called through the prototype chain, this points to the instance that invoked the method:

function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function() {
  console.log(`${this.name} makes a noise`);
};

const dog = new Animal('Dog');
dog.speak(); // Outputs "Dog makes a noise"

this in Asynchronous Contexts

The behavior of this in async functions is consistent with regular functions:

const asyncObj = {
  value: 42,
  async getValue() {
    return this.value; // Correctly points to `asyncObj`
  },
  async getValueArrow: async () => {
    return this.value; // Points to the outer scope's `this`
  }
};

asyncObj.getValue().then(console.log); // 42
asyncObj.getValueArrow().then(console.log); // `undefined` (assuming outer `this` is not `asyncObj`)

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.