这篇我们来深入了解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对象。
两个函数的区别:
- 除了第一个参数外,call 可以接收一个参数列表。
- 除了第一个参数外,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