第1章——this预览
1. this指向
人们很容易误以为this
指向函数自身,不过我们看一下这个例子。
function foo(num){
console.log('foo:'+num)
this.count++
}
foo.count = 0;
var i;
for(i = 0; i < 10; i++){
if(i > 5){
foo(i)
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被调用了多少次?
console.log(foo.count) // 0
console.log
语句产生了4条输出,证明foo(…)确实被调用了4次,但是foo.count是0。
实际上,这段代码无意中在全局创建了一个变量count,他的值为NaN,因为undefined++
= NaN
setTimeout(funciton(){
// 匿名函数,无法指向自身,除非使用arguments.callee
})
var test = 0;
setTimeout(function(){
test++
console.log(test)
test < 5 && arguments.callee()
},10)
如何让例子1中的代码,输入如我们所愿的结果呢(输出4)
function foo(num){
console.log('foo:'+num)
this.count++
}
foo.count = 0;
var i;
for(i = 0; i < 10; i++){
if(i > 5){
foo.call(foo, i) // 将这里进行修改,使用fn.call将指针指向foo函数
}
}
this到底是什么?
学习this的第一步是明白this既指向函数自身,也不指向函数的此法作用域。
this是在运行时进行绑定,而不是在编写时绑定的,它的上下文取决于函数调用时的各种条件。
this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,引擎会创建一个执行上下文,这个记录会包含函数在哪里被调用(调用栈),函数的调用方式,传入的参数等信息。
第二章——this全面解析
调用位置
在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置。
最重要的是对调用站进行分析,我们可以看一个例子
function baz(){
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log("baz")
bar(); // bar的调用位置
}
function bar(){
// 当前的调用栈是 baz -> bar
// 因此当前调用位置在baz中
console.log('bar')
foo() // foo的调用位置
}
window.a = 'a'
function foo(){
// 我们可以在这里加个断点看下
debugger
// 当前调用栈 baz -> bar -> foo
// 因此,当前调用位置在bar中
console.log('foo')
}
baz() // baz的调用位置
这便是我们的**调用栈(call stack)**⬇️
什么是调用栈呢?
根据MDN的解释:
调用栈是解释器(比如浏览器中的 JavaScript 解释器)追踪函数执行流的一种机制。当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。
-
每调用一个函数,解释器就会把该函数添加进调用栈(栈顶)并开始执行。
-
正在调用栈中执行的函数还调用了其它函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行。
-
当前函数执行完毕后,解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。
-
当分配的调用栈空间被占满时,会引发“堆栈溢出”错误。
栈是一种后进先出的数据结构
举个例子:
function f1(){
console.log('Hi by f1!')
}
function f2(){
f1()
console.log('Hi by f2!')
}
f2()
这段代码的执行过程是
- f2函数被调用,因此全局作用域会创建一个执行上下文,并将这个上下文推进执行栈的底部。
- f2执行过程,发现f1函数被调用,因此会将执行上下文推进执行栈,在全局上下文的上面
- f1执行,也创建了一个执行上下文,在f2函数执行上下文的上面
- f1执行完毕,f1的执行上下文被释放,出栈
- f2执行完毕,f2的执行上下文被释放,出栈
this的绑定规则(重点)
this绑定分为4类,他的最终指向取决于他所属分类的优先级。
- 默认绑定
- 隐式绑定
- 显示绑定
- new绑定
1. 默认绑定
例子:
function foo(){
console.log( this.a );
}
var a = 2; // 在全局作用域中声明的属性,相当于window.a = 2
foo(); // 2
这段代码中,foo函数执行,打印出的结果是2,也就是全局作用域中的a(this.a -> window.a)。
为什么foo执行后,this指针指向了全局呢?
在这个例子中,函数调用使用了this的默认绑定,我们可以分析foo函数的调用位置,foo()是直接使用不带任何修饰的函数引用(不通过xxx.foo()调用,不通过foo.call, new foo()等…)进行调用的,因此只能使用默认绑定,无法应用其他规则。
而如果在严格模式下,this.a会打印出undefined
function foo(){
"use strict";
console.log(this.a);
}
var a = 2;
foo() // TypeError: this is undefined
2.隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导。
思考代码:
function foo(){
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
obj.foo() // 2
在这个例子中,调用位置会使用obj上下文来引用函数,当foo()调用,它的指针指向obj对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。
现在我们将刚刚的例子进行一个改造:
function foo(){
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
window.a = 123
var bar = obj.foo
obj.foo()
bar()
结果为
// 2
// 123
为什么第二次打印出window的a,而不是obj的a?
隐式丢失
隐式绑定的函数会丢失绑定对象,他会应用默认绑定,从而把this绑定到全局对象或者undefined(非严格模式)上。
虽然bar是obj.foo的一个引用,但是实际上,他引用的是foo函数本身,因为此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
在使用回调函数时,也会发生这种情况
例子:
function foo(){
console.log(this.a)
}
function doFoo(fn) {
// fn引用的是foo
fn(); // 调用位置
}
var obj = {
a: 2,
foo: foo
}
var a = 'global...'
doFoo(obj.foo) // global...
这里例子中,我们期望打印出obj.a,但结果却是window.a,其实和上个例子一样。
在doFoo(obj.foo)这行代码执行的时候,向doFoo函数传递了obj.foo这个实参,形参赋值的过程就相当于fn = obj.foo
,这时候就发生了和上面例子一样的情况,出现了隐式丢失。
setTimeout,setInterval也会出现this的丢失
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"
回调函数丢失 this 绑定是非常常见的。
隐式绑定的一个注意点
对象属性引用链只有在最顶层或者最后一层会影响调用位置。
例子:
function foo() {
console.log( this.a )
}
var obj2 = {
a: 42,
foo: foo
}
var obj1 = {
a: 2,
obj2: obj2
}
obj1.obj2.foo() // 42
打印出的是obj2的a
3.显示绑定
如果我们希望将this强制绑定在某个对象上,我们可以使用函数的call,apply,bind方法。
function foo(){
console.log(this.a)
}
var obj = {
a: 2
}
foo.cal( obj ) //2
通过foo.call(…),我们可以在调用foo时强制把它的this绑定到obj上。
如果传入了一个原始值(字符串,布尔类型)来当做this的绑定对象,这个原始值会被转为它的对象形式(也就是 new String(…),new Boolean(…))。这通常被称为“装箱”。
硬绑定
function foo(){
console.log(this.a)
}
var obj = {
a: 2
}
var bar = foo.bind(obj)
bar() // 2
setTimeout(bar,100) // 2
window.a = '123'
bar.call(window) // 2
我们通过bind
,在声明bar的时候强行将foo指向了obj,后面再执行的时候就不会发生隐式丢失了。
bind其他用法
function add(...args){
return args.reduce((sum, cur) => sum+cur, 0)
}
var newAdd = add.bind(null, 1,2)
newAdd(3,4)
当我们要使用add函数,且使用时经常用到1,2这两个参数时,我们可以利用bind,让新声明的函数默认有1,2这两个参数,这么做在不改变add函数的基础上,让函数的调用更方便。
4.new绑定
说说new
在传统的面向类的语言中,“构造函数”是类中的一种特殊方法,使用new初始化类时会调用类中的构造函数。通常形式是:
var something = new MyClass(...)
JavaScript也有new,但是JavaScript中new的机制实际上和面向类的语言完全不同。
在JavaScript中,构造函数只是一些使用new操作符时被调用的函数。他们不属于某个类,也不会实例化一个类,他们只是被new操作符调用的普通函数而已。
当函数通过new来调用时,这种函数调用被称为构造函数调用。有一个重要但是非常细微的注意点:
实际上并不存在所谓的“构造函数”,只有对函数的“构造调用”。
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建一个全新的对象
- 这个新对象会被执行[[原型]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数会自动返回这个新对象
例子:
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a) // 2
使用new来调用foo时,会构造新对象并把它绑定到foo(…)调用的this上。称为new绑定。
4种this绑定的优先级
例子1:
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
从上面例子中可以看到,obj1.foo为隐式绑定,obj1.foo.call为显示绑定。最后obj1.foo.call(obj2)结果为3,说明
显式绑定优先级 > 隐式绑定
现在我们比较一下new绑定和隐式绑定优先级:
function foo(sth){
this.a = sth
}
var obj1 = {
foo: foo
}
var obj2 = {}
obj1.foo(2)
console.log(obj1.a) // 2
var bar = new obj1.foo(4)
console.log(obj1.a) // 2
console.log(bar.a) // 4
可以看到new绑定比隐式绑定优先级高,但是new绑定和显示绑定优先级谁更高呢?
function foo(sth){
this.a = sth
}
var obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2
var baz = new bar(3)
console.log(obj1.a) // 2
console.log(baz.a) // 3
bar被硬绑定到obj1上,但是new bar(3)并没有像我们预计的那样把obj1.a修改为3。相反,new修改了硬绑定(到obj1的)调用bar(…)中的this。因为使用了new绑定,我们得到了一个名字为baz的新对象,并且baz.a的值是3。
看一下bind的polyfill实现
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== "function") {
// 与 ECMAScript 5 最接近的
// 内部 IsCallable 函数
throw new TypeError(
"Function.prototype.bind - what is trying " +
"to be bound is not callable"
);
}
var aArgs = Array.prototype.slice.call( arguments, 1 ),
fToBind = this,
fNOP = function(){},
fBound = function(){
return fToBind.apply(
( // 这里是new修改this的相关代码
this instanceof fNOP &&
oThis ? this : oThis
),
aArgs.concat(
Array.prototype.slice.call( arguments )
)
)};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
下面是 new 修改 this 的相关代码:
this instanceof fNOP &&
oThis ? this : oThis
// ... 以及:
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
这段代码会判断绑定函数是否是被new调用,如果是的话就使用新创建的this替换硬绑定的this。
总结判断this的顺序
1.函数是否在new中调用(var bar = new foo())
2.是否通过call,apply,bind绑定调用(var bar = foo.call(obj2))
3.是否在上下文对象中调用(var bar = obj1.foo())
4.以上都不是,使用默认绑定,非严格模式下绑定到全局
箭头函数中的this
function foo() {
// 返回一个箭头函数
return a => {
// this继承自foo()
console.log(this.a)
}
}
var obj1 = {
a: 2
}
var obj2 = {
a: 3
}
var bar = foo.call(obj1)
bar.call(obj2) // 2, 不是3!
foo内部创建的箭头函数会捕获调用时foo()的this,由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new 也不行)
拓展——软绑定
硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。如果可以给默认绑定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕获所有 curried 参数
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 );
// name: obj <---- 应用了软绑定
this的面试题
转载自:
【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)
题1:
考察了在全局作用域中开启严格模式,对this有哪些改变。
"use strict";
var a = 10;
function foo () {
console.log('this1', this)
console.log(window.a)
console.log(this.a)
}
console.log(window.foo)
console.log('this2', this)
foo();
var a = 10 依然是相当于执行代码 window.a = 10,开启严格模式只使得函数中的this
指向undefined
,并不会改变全局中this
的指向。因此this2
还是window
对象。
题2:
let a = 10
const b = 20
function foo(){
console.log(this.a)
console.log(this.b)
}
foo();
console.log(window.a)
解析:
如果把var改成let或const,变量不会被绑定到window上。
结果:
undefined
undefined
undefined
题3:
var a = 1;
function foo(){
var a = 2;
function inner() {
console.log(this.a)
}
inner()
}
foo()
答案:1
这里相当于执行
题4
var a = 1;
function foo(){
this.a = 2;
function inner() {
console.log(this.a)
}
inner()
}
foo()
答案: 2
题5:
var obj = {
a: 1,
b: {
a: 2,
getA: getA
},
c:{
getA: getA
}
}
obj.b.getA()
obj.c.getA()
答案:
2
undefined
题6
function foo () {
console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;
var obj2 = { a: 3, foo2: obj.foo }
obj.foo();
foo2();
obj2.foo2();
解析foo2()
和obj2
中的foo
都发生了隐式丢失,容易忽略的是obj2.foo2()
的this
在丢失后,由于是obj2
调用了foo2
,所以this
指针又指向了obj2
结果
1
2
3
未完待续…