this
介绍:JavaScript 语言之所以有this的设计,跟内存里面的数据结构有关系。
var obj = { foo: 5 };
上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。
也就是说,变量obj是一个地址。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。
概念:原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的。
{
foo: {
[[value]]: 5
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
注意:foo属性的值保存在属性描述对象的value属性里面。
这样的结构是很清晰的,问题在于属性的值可能是一个函数。
var obj = { foo: function () {} };
这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。
{
foo: {
[[value]]: 函数的地址
...
}
}
由于函数也是一个对象是一个单独的值,所以它可以在不同的环境(上下文)执行。
var f = function () {};
var obj = { f: f };
// 单独执行
f()
// obj 环境执行
obj.f()
思考:JavaScript 允许在函数体内部,引用当前环境的其他变量。下面代码中,函数体里面使用了变量x。该变量由运行环境提供。那么,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行上下文环境。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
var f = function () {
console.log(x);
};
下面代码中,函数体里面的this.x就是指当前运行环境的x。
var f = function () {
console.log(this.x);
}
例子:下面代码中,函数f在全局环境执行,this.x指向全局环境的x。在obj环境执行,this.x指向obj.x。
var f = function () {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
};
// 单独执行
f() // 1
// obj 环境执行
obj.f() // 2
总结:我们可以这样理解,因为obj.foo()是通过obj找到foo,所以就是在obj环境执行。一旦var foo = obj.foo,变量foo就直接指向函数本身,所以foo()就变成在全局环境执行。
this的指向问题
概念:在函数中除了声明时定义的形式参数,每个函数还接受2个附加的参数:this 和 argument。其中 this 在面向对象编程中非常重要,他的值取决于调用的模式共四种:
- 方法调用模式
- 函数调用模式
- 构造器调用模式
- apply调用模式
以上四种调用模式在如何初始化关键参数this上存在差异
方法调用模式
介绍:当一个函数被保存微对象的一个属性时,我们称其为一个方法。当一个方法被调用时,this被绑定到该对象。如果调用表达式包含一个提取属性的动作(即包含一个.
表达式或[subscript]
下标表达式,那么他就是被当做一个方法来调用)
此时方法可以使用this访问自己所属的对象,所以他能从对象中取值对对象进行修改
(this到对象的绑定发生在调用的时候,此特性可以使得函数可以对this高度复用。通过this可取得他们所属对象的上下文的方法称为公共方法)
var mayObj = {
value: 0,
increment: function(inc) {
this.value += typeof inc === 'number'? inc : 1
}
}
myObj.increment()
console.log(myObj.value) // 1
myObj.increment(2)
console.log(myObj.value) // 3
函数调用模式
介绍:当一个函数并未一个对象的属性时,那么他就是被当做一个函数来调用的,以此模式调用函数时。this会被绑定到全局对象
注意:这种this的绑定是语言设计上的错误,因为如果语言设计正确,那么当函数被调用时,this应该仍然绑定到外部函数的this变量,这个设计的错误后果就是方法不能利用内部函数来帮助他工作,因为内部函数的this被绑定的错误的值,所以不能共享该方法对对象的访问权
构造器调用模式
介绍:javascript 是一门基于原型继承的语言,所以对象可以直接从其他对象继承属性,单当今大多数语言都是基于类的语言。虽然原型继承极富表现力,但他并未被广泛理解,所以JavaScript提供了一套基于类的语言的构建语法。如果在一个函数前面加上 new
关键字来调用,那么函数将会连接到prototype 成员的新对象,同时this会被绑定到这个新对象上
一个函数如果创建的目的就是希望结合new 前缀来调用,那他就被称为构造器函数。按照约定,他们保存在以大写格式命名的变量里。如果调用构造器函数没有在前面加上new,可能会发生非常糟糕的事情。
Apply 调用模式
因为JavaScript是一门函数式的面向对象编程语言,所以函数可以拥有方法,apply方法让我们构建一个参数数组传递给调用函数,它允许我们选择this的值,apply方法接受两个参数。第一个是要绑定给this的值,第二个就是一个参数数组
apply
介绍:apply() 方法调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数。
语法:
func.apply(thisArg, [argsArray])
- 参数
- thisArg: 在 func 函数运行时使用的 this 值。
请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象。
- argsArray:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。
- 返回值 指定this值和参数后的func函数调用后的返回值。
例子:
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.apply(this, [name, price]);
this.category = 'food';
}
console.log(new Food('cheese', 5).name);
call
介绍:call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
注意:该方法的语法和作用与 apply() 方法类似,只有一个区别,就是 call() 方法接受的是一个参数列表,而 apply() 方法接受的是一个包含多个参数的数组。
语法:
function.call(thisArg, arg1, arg2, ...)
- 参数
- thisArg 在 function 函数运行时使用的 this 值。
- arg1, arg2, … 指定的参数列表。
- 返回值 指定this值和参数后的func函数调用后的返回值。
例子:
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}
console.log(new Food('cheese', 5).name);
bind
介绍:bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
语法
function.bind(thisArg[, arg1[, arg2[, ...]]])
-
参数
- thisArg 调用绑定函数时作为 this 参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。如果thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。
- arg1, arg2, … 当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
-
返回值返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
例子:
const module = {
x: 42,
getX: function() {
return this.x;
}
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // 该函数在全局作用域被调用
// undefined
const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// 42