更新:谢谢你们的支持,最近折腾了一个博客官网出来,方便你们系统阅读,后续会有更多内容和更多优化,猛戳这里查看前端
------ 如下是正文 ------webpack
this
的绑定规则总共有下面5种。git
调用位置就是函数在代码中被调用的位置(而不是声明的位置)。github
查找方法:web
分析调用栈:调用位置就是当前正在执行的函数的前一个调用中面试
function baz() {
// 当前调用栈是:baz
// 所以,当前调用位置是全局做用域
console.log( "baz" );
bar(); // <-- bar的调用位置
}
function bar() {
// 当前调用栈是:baz --> bar
// 所以,当前调用位置在baz中
console.log( "bar" );
foo(); // <-- foo的调用位置
}
function foo() {
// 当前调用栈是:baz --> bar --> foo
// 所以,当前调用位置在bar中
console.log( "foo" );
}
baz(); // <-- baz的调用位置
复制代码
使用开发者工具获得调用栈:算法
设置断点或者插入debugger;
语句,运行时调试器会在那个位置暂停,同时展现当前位置的函数调用列表,这就是调用栈。找到栈中的第二个元素,这就是真正的调用位置。跨域
undefined
。只有函数运行在非严格模式下,默认绑定才能绑定到全局对象。在严格模式下调用函数则不影响默认绑定。function foo() { // 运行在严格模式下,this会绑定到undefined
"use strict";
console.log( this.a );
}
var a = 2;
// 调用
foo(); // TypeError: Cannot read property 'a' of undefined
// --------------------------------------
function foo() { // 运行
console.log( this.a );
}
var a = 2;
(function() { // 严格模式下调用函数则不影响默认绑定
"use strict";
foo(); // 2
})();
复制代码
当函数引用有上下文对象时,隐式绑定规则会把函数中的this绑定到这个上下文对象。对象属性引用链中只有上一层或者说最后一层在调用中起做用。数组
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
复制代码
隐式丢失浏览器
被隐式绑定的函数特定状况下会丢失绑定对象,应用默认绑定,把this绑定到全局对象或者undefined上。
// 虽然bar是obj.foo的一个引用,可是实际上,它引用的是foo函数自己。
// bar()是一个不带任何修饰的函数调用,应用默认绑定。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名
var a = "oops, global"; // a是全局对象的属性
bar(); // "oops, global"
复制代码
参数传递就是一种隐式赋值,传入函数时也会被隐式赋值。回调函数丢失this绑定是很是常见的。
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn其实引用的是foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a是全局对象的属性
doFoo( obj.foo ); // "oops, global"
// ----------------------------------------
// JS环境中内置的setTimeout()函数实现和下面的伪代码相似:
function setTimeout(fn, delay) {
// 等待delay毫秒
fn(); // <-- 调用位置!
}
复制代码
经过call(..)
或者 apply(..)
方法。第一个参数是一个对象,在调用函数时将这个对象绑定到this。由于直接指定this的绑定对象,称之为显示绑定。
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); // 2 调用foo时强制把foo的this绑定到obj上
复制代码
显示绑定没法解决丢失绑定问题。
解决方案:
建立函数bar(),并在它的内部手动调用foo.call(obj),强制把foo的this绑定到了obj。这种方式让我想起了借用构造函数继承,没看过的能够点击查看 JavaScript经常使用八种继承方案
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的bar不可能再修改它的this
bar.call( window ); // 2
复制代码
典型应用场景是建立一个包裹函数,负责接收参数并返回值。
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = function() {
return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5
复制代码
建立一个能够重复使用的辅助函数。
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
return function() {
return fn.apply( obj, arguments );
}
}
var obj = {
a: 2
};
var bar = bind( foo, obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
复制代码
ES5内置了Function.prototype.bind
,bind会返回一个硬绑定的新函数,用法以下。
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
复制代码
JS许多内置函数提供了一个可选参数,被称之为“上下文”(context),其做用和bind(..)
同样,确保回调函数使用指定的this。这些函数实际上经过call(..)
和apply(..)
实现了显式绑定。
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
}
var myArray = [1, 2, 3]
// 调用foo(..)时把this绑定到obj
myArray.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
复制代码
构造函数
只是使用new
操做符时被调用的普通
函数,他们不属于某个类,也不会实例化一个类。Number(..)
)在内的全部函数均可以用new
来调用,这种函数调用被称为构造函数调用。使用new
来调用函数,或者说发生构造函数调用时,会自动执行下面的操做。
[[Prototype]]
链接。this
。new
表达式中的函数调用会自动返回这个新对象。使用new
来调用foo(..)
时,会构造一个新对象并把它(bar
)绑定到foo(..)
调用中的this。
function foo(a) {
this.a = a;
}
var bar = new foo(2); // bar和foo(..)调用中的this进行绑定
console.log( bar.a ); // 2
复制代码
手写一个new实现
function create() {
// 建立一个空的对象
var obj = new Object(),
// 得到构造函数,arguments中去除第一个参数
Con = [].shift.call(arguments);
// 连接到原型,obj 能够访问到构造函数原型中的属性
obj.__proto__ = Con.prototype;
// 绑定 this 实现继承,obj 能够访问到构造函数中的属性
var ret = Con.apply(obj, arguments);
// 优先返回构造函数返回的对象
return ret instanceof Object ? ret : obj;
};
复制代码
使用这个手写的new
function Person() {...}
// 使用内置函数new
var person = new Person(...)
// 使用手写的new,即create
var person = create(Person, ...)
复制代码
代码原理解析:
一、用new Object()
的方式新建了一个对象obj
二、取出第一个参数,就是咱们要传入的构造函数。此外由于 shift 会修改原数组,因此 arguments
会被去除第一个参数
三、将 obj
的原型指向构造函数,这样obj
就能够访问到构造函数原型中的属性
四、使用apply
,改变构造函数this
的指向到新建的对象,这样 obj
就能够访问到构造函数中的属性
五、返回 obj
st=>start: Start
e=>end: End
cond1=>condition: new绑定
op1=>operation: this绑定新建立的对象,
var bar = new foo()
cond2=>condition: 显示绑定
op2=>operation: this绑定指定的对象,
var bar = foo.call(obj2)
cond3=>condition: 隐式绑定
op3=>operation: this绑定上下文对象,
var bar = obj1.foo()
op4=>operation: 默认绑定
op5=>operation: 函数体严格模式下绑定到undefined,
不然绑定到全局对象,
var bar = foo()
st->cond1
cond1(yes)->op1->e
cond1(no)->cond2
cond2(yes)->op2->e
cond2(no)->cond3
cond3(yes)->op3->e
cond3(no)->op4->op5->e
复制代码
在new
中使用硬绑定函数的目的是预先设置函数的一些参数,这样在使用new
进行初始化时就能够只传入其他的参数(柯里化)。
function foo(p1, p2) {
this.val = p1 + p2;
}
// 之因此使用null是由于在本例中咱们并不关心硬绑定的this是什么
// 反正使用new时this会被修改
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2
复制代码
把null
或者undefined
做为this
的绑定对象传入call
、apply
或者bind
,这些值在调用时会被忽略,实际应用的是默认规则。
下面两种状况下会传入null
apply(..)
来“展开”一个数组,并看成参数传入一个函数bind(..)
能够对参数进行柯里化(预先设置一些参数)function foo(a, b) {
console.log( "a:" + a + ",b:" + b );
}
// 把数组”展开“成参数
foo.apply( null, [2, 3] ); // a:2,b:3
// 使用bind(..)进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2,b:3
复制代码
老是传入null
来忽略this绑定可能产生一些反作用。若是某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象中。
更安全的this
安全的作法就是传入一个特殊的对象(空对象),把this绑定到这个对象不会对你的程序产生任何反作用。
JS中建立一个空对象最简单的方法是**Object.create(null)
**,这个和{}
很像,可是并不会建立Object.prototype
这个委托,因此比{}
更空。
function foo(a, b) {
console.log( "a:" + a + ",b:" + b );
}
// 咱们的空对象
var ø = Object.create( null );
// 把数组”展开“成参数
foo.apply( ø, [2, 3] ); // a:2,b:3
// 使用bind(..)进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2,b:3
复制代码
间接引用下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生。
// p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo()
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4};
o.foo(); // 3
(p.foo = o.foo)(); // 2
复制代码
new
除外),防止函数调用应用默认绑定规则。可是会下降函数的灵活性,使用硬绑定以后就没法使用隐式绑定或者显式绑定来修改this。// 默认绑定规则,优先级排最后
// 若是this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,不然不会修改this
if(!Function.prototype.softBind) {
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;
};
}
复制代码
使用:软绑定版本的foo()能够手动将this绑定到obj2或者obj3上,但若是应用默认绑定,则会将this绑定到obj。
function foo() {
console.log("name:" + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
// 默认绑定,应用软绑定,软绑定把this绑定到默认对象obj
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
复制代码
ES6新增一种特殊函数类型:箭头函数,箭头函数没法使用上述四条规则,而是根据外层(函数或者全局)做用域(词法做用域)来决定this。
foo()
内部建立的箭头函数会捕获调用时foo()
的this。因为foo()
的this绑定到obj1
,bar
(引用箭头函数)的this也会绑定到obj1
,箭头函数的绑定没法被修改(new
也不行)。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!
复制代码
ES6以前和箭头函数相似的模式,采用的是词法做用域取代了传统的this机制。
function foo() {
var self = this; // lexical capture of this
setTimeout( function() {
console.log( self.a ); // self只是继承了foo()函数的this绑定
}, 100 );
}
var obj = {
a: 2
};
foo.call(obj); // 2
复制代码
代码风格统一问题:若是既有this风格的代码,还会使用 seft = this
或者箭头函数来否认this机制。
bind(..)
,尽可能避免使用 self = this
和箭头函数。代码1:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
复制代码
代码2:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
复制代码
上面的两个代码中,checkscope()
执行完成后,闭包f
所引用的自由变量scope
会被垃圾回收吗?为何?
解答:
checkscope()
执行完成后,代码1中自由变量特定时间以后回收,代码2中自由变量不回收。
首先要说明的是,如今主流浏览器的垃圾回收算法是标记清除,标记清除并不是是标记执行栈的进出,而是从根开始遍历,也是一个找引用关系的过程,可是由于从根开始,相互引用的状况不会被计入。因此当垃圾回收开始时,从Root(全局对象)开始寻找这个对象的引用是否可达,若是引用链断裂,那么这个对象就会回收。
闭包中的做用域链中 parentContext.vo 是对象,被放在堆中,栈中的变量会随着执行环境进出而销毁,堆中须要垃圾回收,闭包内的自由变量会被分配到堆上,因此当外部方法执行完毕后,对其的引用并无丢。
每次进入函数执行时,会从新建立可执行环境和活动对象,但函数的[[Scope]]
是函数定义时就已经定义好的(词法做用域规则),不可更改。
checkscope()
执行时,将checkscope
对象指针压入栈中,其执行环境变量以下
checkscopeContext:{
AO:{
arguments:
scope:
f:
},
this,
[[Scope]]:[AO, globalContext.VO]
}
复制代码
执行完毕后出栈,该对象没有绑定给谁,从Root开始查找没法可达,此活动对象一段时间后会被回收
checkscope()
执行后,返回的是f
对象,其执行环境变量以下
fContext:{
AO:{
arguments:
},
this,
[[Scope]]:[AO, checkscopeContext.AO, globalContext.VO]
}
复制代码
此对象赋值给var foo = checkscope();
,将foo
压入栈中,foo
指向堆中的f
活动对象,对于Root
来讲可达,不会被回收。
若是必定要自由变量scope
回收,那么该怎么办???
很简单,foo = null;
,把引用断开就能够了。
依次给出console.log输出的数值。
var num = 1;
var myObject = {
num: 2,
add: function() {
this.num = 3;
(function() {
console.log(this.num);
this.num = 4;
})();
console.log(this.num);
},
sub: function() {
console.log(this.num)
}
}
myObject.add();
console.log(myObject.num);
console.log(num);
var sub = myObject.sub;
sub();
复制代码
进阶系列文章汇总以下,内有优质前端资料,以为不错点个star。
我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!