回顾JavaScript中的this、call、apply与bind

114 阅读5分钟

一、this关键字详解

this关键字在JavaScript中用于引用当前对象的上下文。它的值取决于函数调用的方式,而不是函数定义的方式。这使得this成为JavaScript中一个既强大又容易令人困惑的特性。

1.1 this的基本用法

在全局上下文中,this通常指向全局对象(在浏览器中为window对象)。

	var name = "Global";
	console.log(this.name); // 输出: Global

在函数内部,this的值取决于函数是如何被调用的。

	function sayHello() {
	  console.log(this.name);
	}

	var person = {
	  name: "Alice",
	  sayHello: sayHello
	};
	 
	sayHello(); // 输出: undefined(在严格模式下),非严格模式下可能是全局对象(在浏览器中是 window)
	person.sayHello(); // 输出: Alice
1.2 this的绑定规则

JavaScript中this的绑定规则主要有以下几种:

  • 默认绑定:当函数独立调用时,this通常指向全局对象(在严格模式下为undefined)。
	function foo() {
	  console.log(this.a);
	}
	var a = 2;
	foo(); // 输出: 2(非严格模式),严格模式下会报错
  • 隐式绑定:当函数作为对象的方法被调用时,this指向调用该方法的对象。
	function foo() {
	  console.log(this.a);
	}
	var obj = {
	  a: 2,
	  foo: foo
	};
	obj.foo(); // 输出: 2
  • 显式绑定:使用callapplybind方法显式地指定this的值。
	function foo() {
	  console.log(this.a);
	}
	var obj = {
	  a: 2
	};
	foo.call(obj); // 输出: 2
  • new绑定:当使用new关键字调用函数时,this指向新创建的实例对象。
	function Person(name) {
	  this.name = name;
	}
	var person = new Person("Alice");
	console.log(person.name); // 输出: Alice
1.3 this的常见陷阱
  • 丢失绑定:当函数作为回调函数传递时,可能会丢失原有的this绑定。
	function foo() {
	  console.log(this.a);
	}
	var obj = {
	  a: 2,
	  foo: foo
	};

	setTimeout(obj.foo, 1000); // 输出: undefined,因为setTimeout中的this指向window
  • 嵌套函数中的this:嵌套函数中的this不会继承外层函数的this绑定。
	function foo() {
	  console.log(this.a);
	  function bar() {
	    console.log(this.a);
	  }
	  bar();
	}
	var obj = {
	  a: 2
	};
	foo.call(obj); // 输出: 2(foo中的this),undefined(bar中的this)

二、call方法详解

call方法用于调用一个函数,并显式地指定函数内部this的值,同时传递参数(参数以逗号分隔)。

2.1 call的基本语法
	function.call(thisArg, arg1, arg2, ...)
  • thisArg:在函数运行时指定的this值。
  • arg1, arg2, ...:传递给函数的参数。
2.2 call的示例代码
	function greet(greeting, punctuation) {
	  console.log(greeting + ', ' + this.name + punctuation);
	}
	const person = {
	  name: 'Bob'
	};
	greet.call(person, 'Hello', '!'); // 输出: Hello, Bob!
2.3 call的场景应用
  • 继承:使用call方法实现对象之间的继承。
	function Person(name) {
	  this.name = name;
	}
	function Student(name, age) {
	  Person.call(this, name); // 继承Person的属性
	  this.age = age;
	}
	const student = new Student('Alice', 20);
	console.log(student.name); // 输出: Alice
  • 数组方法借用:使用call方法借用数组的方法来处理类数组对象。
	function Person(name, age) {
	  this.name = name;
	  this.age = age;
	}
	const people = [
	  new Person('Alice', 25),
	  new Person('Bob', 30)
	];
	const names = Array.prototype.map.call(people, function(person) {
	  return person.name;
	});
	console.log(names); // 输出: ['Alice', 'Bob']

三、apply方法详解

apply方法与call类似,但传递参数的方式不同。apply是以数组的形式传递参数。

3.1 apply的基本语法
	function.apply(thisArg, [argsArray])
  • thisArg:在函数运行时指定的this值。
  • argsArray:一个数组或类数组对象,其中的数组元素将作为单独的参数传递给函数。
3.2 apply的示例代码
	function greet(greeting, punctuation) {
	  console.log(greeting + ', ' + this.name + punctuation);
	}
	const person = {
	  name: 'Charlie'
	};
	greet.apply(person, ['Hi', '?']); // 输出: Hi, Charlie?
3.3 apply的场景应用
  • 数学计算:使用apply方法将数组中的元素作为参数传递给数学函数。
	const numbers = [1, 2, 3, 4];
	const sum = Math.max.apply(null, numbers);
	console.log(sum); // 输出: 4
  • 对象合并:使用apply方法将对象属性合并到另一个对象中。
	const target = { a: 1 };
	const source = { b: 2, c: 3 };
	Object.assign(target, source);
	console.log(target); // 输出: { a: 1, b: 2, c: 3 }

	// 使用apply实现类似功能

	function merge(target, source) {
	  for (let key in source) {
	    if (source.hasOwnProperty(key)) {
	      target[key] = source[key];
	    }
	  }
	}
	const keys = Object.keys(source);
	merge.apply(null, [target].concat(keys.map(key => source[key])));
	console.log(target); // 输出: { a: 1, b: 2, c: 3 }

四、bind方法详解

bind方法创建一个新的函数,当这个新函数被调用时,this关键字被设置为bind的第一个参数,其余参数将作为新函数运行时的初始参数。

4.1 bind的基本语法
	function.bind(thisArg[, arg1[, arg2[, ...]]])
  • thisArg:调用绑定函数时作为this参数传递给目标函数的值。
  • arg1, arg2, ...:当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。
4.2 bind的示例代码
	function greet(greeting, punctuation) {
	  console.log(greeting + ', ' + this.name + punctuation);
	}
	const person = {
	  name: 'David'
	};
	const boundGreet = greet.bind(person, 'Hello');
	boundGreet('!'); // 输出: Hello, David!
4.3 bind的场景应用
  • 事件处理:在事件处理函数中绑定this,确保this指向期望的对象。
	const button = document.querySelector('button');
	function handleClick() {
	  console.log(this.textContent);
	}
	button.addEventListener('click', handleClick.bind(button));
  • 回调函数:在回调函数中使用bind来确保this的正确指向。
	function Person(name) {
	  this.name = name;
	}
	Person.prototype.sayHello = function() {
	  setTimeout(function() {
	    console.log('Hello, my name is ' + this.name);
	  }.bind(this), 1000);
	};
	const person = new Person('Eve');
	person.sayHello(); // 输出: Hello, my name is Eve

五、综合示例与场景应用

为了更全面地理解thiscallapplybind的使用,下面通过一个综合示例来展示它们的实际应用。

5.1 示例场景

假设我们有一个简单的购物车系统,需要实现以下功能:

  1. 添加商品到购物车。
  2. 计算购物车中商品的总价。
  3. 显示购物车中的商品列表。
5.2 代码实现
	// 商品类
	function Product(name, price) {
	  this.name = name;
	  this.price = price;
	}

	// 购物车类
	function Cart() {
	  this.items = [];
	}

	// 添加商品到购物车
	Cart.prototype.addItem = function(product) {
	  this.items.push(product);
	};

	// 计算购物车中商品的总价
	Cart.prototype.getTotal = function() {
	  return this.items.reduce((total, product) => total + product.price, 0);
	};

	// 显示购物车中的商品列表
	Cart.prototype.displayItems = function() {
	  return this.items.map(product => `${product.name}: $${product.price}`).join('\n');
	};

	// 创建商品实例
	const product1 = new Product('Laptop', 999);
	const product2 = new Product('Smartphone', 499);

	// 创建购物车实例

	const cart = new Cart();

	// 使用call方法添加商品到购物车(模拟继承)
	function addToCart(product) {
	  Cart.prototype.addItem.call(this, product);
	}

	// 使用apply方法计算购物车总价(处理动态参数)
	function calculateTotal(products) {
	  return Cart.prototype.getTotal.apply(this, [products]);
	}

	// 使用bind方法绑定购物车实例到显示商品列表函数
	const displayCartItems = Cart.prototype.displayItems.bind(cart);

	// 添加商品到购物车
	addToCart.call(cart, product1);
	addToCart.call(cart, product2);

	// 计算购物车总价
	console.log(`Total: $${calculateTotal.call(cart, cart.items)}`);

	// 显示购物车商品列表
	console.log(displayCartItems());
5.3 代码解析
  • 商品类:定义了一个简单的Product类,用于表示商品及其价格。
  • 购物车类:定义了一个Cart类,包含添加商品、计算总价和显示商品列表的方法。
  • 添加到购物车:使用call方法模拟继承,将Cart类的addItem方法应用到当前上下文(购物车实例)。
  • 计算总价:使用apply方法处理动态参数列表,将商品数组作为参数传递给getTotal方法。
  • 显示商品列表:使用bind方法将displayItems方法绑定到购物车实例,以便在调用时自动传递正确的this值。