问:什么是作用域?
答:作用域就是代码中定义变量的区域
问:作用域用来干啥?
答:作用域规定了当前变量的可访问范围以及权限
问:作用域有几种?
答:词法作用域,动态作用域,js中是词法作用域(也叫静态作用域)
问:这两种有什么区别?
答:词法作用域中函数的作用域在定义的时候就规定好了,而动态作用域的话需要运行的时候才能定义
问:作用域和上下文?
答:作用域和上下文不是一个概念,作用域从字面意思都能感觉到指的是一个区域,而上下文一般都是this,this就是指向当前函数的调用对象的一个引用,这叫上下文
so what is this(context)?
js中this是一直是一个比较神(che)奇(dan)的存在,但是如果你仔细去理解一遍的话,你会发现这些magic其实也不是什么神奇的事情,搞定它,你才能舒服的把玩js,this在js中的意义非常简单,永远指向函数的调用者,所以,要搞清楚this是什么 必须搞清楚当前函数的调用对象是哪个???
下面我们结合实例来看看神奇的this
eg1:
function foo(){ console.log(this.a);}var a = 2;foo();???复制代码
在控制台尝试一下 发现打印出了2,所以this是什么?首先这里其实隐藏了2个小点,(1)默认在非严格模式下定义一个变量是会把这个变量定义在window上的,这里其实window.a就是2;(2)js中存在着变量的隐式提升,所有的变量会隐式的提升到当前作用域的顶端,所以其实执行的顺序是这样的
var a;function foo(){ console.log(this.a);}a = 2; // 这个时候 window.a = 2了foo(); // 输出window.a复制代码
总结1 默认情况下this是window,这是this的默认绑定规则
eg2:
var a = 3;function foo(){ console.log(this.a);}var obj = { a:2, foo: foo,}obj.foo();复制代码
按照上面说的默认绑定规则这个地方应该输出3,实际运行发现输出的是2,这里介绍this的第二种绑定方式,隐式绑定,还记得一开始说过的,this永远指向调用该函数的对象,所以,这里调用foo的对象是obj,实际上foo并不属于obj,但是obj的foo属性指向了foo方法,因此obj调用foo的时候this被绑定到obj,所以最后打印this.a其实就是obj.a,当然输出是2了
eg3:
var a = 3;function foo() { console.log(this.a);}var obj = { a: 2, foo: foo,}var obj2 = { a: 4, obj: obj,}obj2.obj.foo();复制代码
这里需要注意的是隐式绑定规则只对对象属性引用链中的最后一层起作用,也就是说当前输出的是2 并不是4,当前的this还是obj而不是obj2,如果想让他指向obj2我们必须采取一点手段,考虑下面这个函数
eg4:
var a = 3;function foo() { console.log(this.a);}var obj = { a: 2, foo: foo,}var foo1 = obj.foo;foo1();复制代码
注意这一步, var foo1 = obj.foo,这一步只是一个函数的别名,现在foo1指向的也是foo这个函数了,但是执行foo1的时候,当前调用foo1的在默认绑定的原则下是window(取决于是否是严格模式),所以当前输出的是3
总结2 隐式情况下,如果包含this的function被某一个对象调用了,那么this指向的就是当前调用对象,这是this的隐式绑定规则。但是要注意如果是函数别名,或者在执行默认绑定的时候改变了函数的调用位置,就要注意当前的this指向问题了~这就是this的隐式丢失问题
显示绑定,上面的例子中,当我们用obj去调用foo的时候,实际上对象和函数之间是通过来关联的,同时js也给我们提供了改变这种特性的一些方法,比较常用的有call和apply,任何一个你写的js函数都有这两个方法,它不会随宿主环境发生变化,同时这2个函数都可以显示的改变当前函数的调用对象,我们称之为显示的硬绑定
eg5:
function foo() { console.log(this.a);}var obj = { a: 2, foo: foo,}var obj1 = { a: 3, foo: foo,}obj.foo.call(obj1); // 3复制代码
这个地方隐式绑定会使得obj在调用foo的时候,this指向obj本身,但是call方法显示的改变了this的指向,让它显示的指向了obj1,因此输出了obj1.a,apply的功能和call是一样的,那么区别是什么,区别在于他们需要后续给函数传递参数的方式不同,call需要依次把参数带上,而apply需要把参数都放在一个数组中;
eg6:
function foo(b, c) { console.log(this.a, b, c);}var obj = { a: 2, foo: foo,}var obj1 = { a: 3, foo: foo,}obj.foo.call(obj1, 'b', 'c'); // 3,'b','c'obj.foo.apply(obj1, ['b', 'c']) // 3,'b','c'复制代码
可以看出都改变了this的指向,同时都把参数带下来了
硬绑定的使用场景,创建一个包裹函数
eg7:
function foo(something) { console.log(this.a, something); return this.a + something;}var obj = { a: 2,}var bar = function(){ return foo.apply(obj, arguments);}console.log(bar(3)); // 5复制代码
实际上,我们常用的办法是使用硬绑定实现一个辅助函数
function bind(fn, obj) { return function () { fn.apply(obj, arguments); }}var obj = { a: 2}function foo(){ console.log(this.a);}var bar = bind(foo, obj)(); // 2复制代码
由于这种bind方式的硬绑定是一个很常见的操作,因此es5提供了内置的bind函数,Function.prototype.bind,当然真实的bind比我们上面写的例子要复杂很多,不过思路是一样的,看下面的例子
eg8:
function foo(el) { console.log(el, this.id);};var obj = { id: 2};[1, 2, 3].forEach(foo);// 1 undefined, 2 undefined, 3 undefined[1, 2, 3].forEach(foo, obj); // 1,2 2,2 3,2复制代码
内置的很多api都会允许我们传入一个上下文,作用和bind是一样的,目的是可以改变回调函数的上下文(是不是没用过?)
总结3:利用call和apply可以显示的改变函数的上下文,使其指向不同的对象,这种绑定方式叫做this的硬绑定。
最后介绍js中最后一种绑定this的方法,叫做 new绑定,在介绍之前,首先要区分清楚传统的面向对象语言的new 和js的new 长得虽然是一样的,但是实际的流程是完全不一样的,js中的对一个function进行new的时候流程是这样的:
1.创建(构造)一个全新的对象;
2.这个对象进行【prototype】连接;
3.绑定这个新对象到函数调用的this;
4.默认情况下,返回这个新的对象;
eg9:
function foo(a) { this.a = a;}var bar = new foo(2);console.log(bar.a); // 2复制代码
总结4 new操作符是最后一种可以改变函数this的办法
-------------------------------分割线-----------------------------
接下来,看下四种this绑定方式的优先级问题
默认绑定是优先级最低的,因为它没有处理任何东西
显示绑定 vs 隐式绑定?
eg10:
function foo() { console.log(this.a);};var obj = { a: 2, foo: foo}var obj1 = { a: 3, foo: foo}obj.foo(); // 2obj.foo.call(obj1); // 3复制代码
明显使用call以后this发生了改变,因此显示的优先级比隐式的优先级要高
隐式绑定 vs new 绑定 eg11:
function foo(sth){ this.a = sth;}var obj = { foo: foo,}obj.foo(2);var bar = new obj.foo(4);console.log(obj.a); // 2console.log(bar.a); // 4复制代码
所以new 绑定比隐式绑定的优先级要高
显式绑定 vs new 绑定
eg12:
function foo(sth) { this.a = sth;}var obj = { foo: foo,}var obj1 = { a: 3}obj.foo.call(obj1);var bar = new obj.foo(4).call(obj1); // error 无法同时使用new 和 callconsole.log(obj.a);console.log(bar.a);复制代码
因为 call 和new 是无法同时使用的,我们换一个思路
eg13:
var obj = {}function foo(sth) { this.a = sth;}var bar = foo.bind(obj);bar(2);console.log(obj.a); // 2var bar1 = new bar(3)console.log(bar1.a); // 3复制代码
此时,bar1 是通过new了一个bind过的函数得到的,但是new出来的bar1中的this明显发生了改变,因此可以得出的结论如下:
总结5 this绑定顺序:new > 显式 > 隐式 > 默认
你以为这就完了吗?
no 有规则就会有例外,比如: null
function foo() { console.log(this.a);}var a = 3;var obj = { a: 2, foo: foo}console.log(obj.foo.call(null)) // 3复制代码
总结6 this显示绑定如果绑定到null或者undefined上,在非严格模式下会使得this绑定到window上去
作用:
- 展开数组
function foo(a, b) { console.log(`a:${a},b:${b}`);}foo.apply(null, [2,3]); // a:2,b:3复制代码
- 函数柯里化
function foo(a, b) { console.log(`a:${a},b:${b}`); return a + b;}var sum = foo.bind(null, 2);console.log(sum(3)); // a:2,b:3 // 5复制代码
。。。
ES6出现了一个新的语法糖,箭头函数,要注意了,上面的所有规则均不适合箭头函数,因为箭头函数是没有this的,它的this完全是上层的this来决定,也就是说它的this是继承上层this来的,看例子
function foo() { return () => { console.log(this.a) }}var obj1 = { a: 2}var obj2 = { a: 3}foo.call(obj1).call(obj2); // !!!输出是2不是3复制代码
你还记得夏明湖畔的self吗?
以前:
function foo() { var self = this; setTimeout(function () { console.log(self.a) }, 0);}var obj = { a: 2}foo.call(obj); // 2复制代码
现在:
function foo() { setTimeout(() => { console.log(this.a) }, 0);}var obj = { a: 2}foo.call(obj); // 2复制代码
------but----- 前方危险,兄弟,箭头函数 请不要乱用~~~
$('body').on(click, () => { console.log(this); // undefined or window})$('body').on(click, function () { console.log(this); // body dom object})复制代码
这里回调的时候,是动态绑定context的,强行使用箭头函数,会导致你在严格模式下什么都拿不到;
const foo = (a) => { this.a = a;}const bar = new foo(1); //error复制代码
构造函数要是没有自己的this?你敢想象吗?
另外,想想: react 为什么要bind this?因为你调用的时候是这个姿势
所以呢,你是直接从react的实例上把function拿出来了,然后点击的时候回调的上下文变了,导致你在这个handleClick中如果使用了this的话,就不对了,当前的this是当前的上下文,并不是react的实例,所以要提前把他bind好(上去找eg4,道理是一样的)
换个姿势
开心的去试吧
嗯~~~ vue 的methods为什么不建议用箭头函数?
methods: { handleClick: () => { console.log(this) // undefined }}复制代码
哪里来的this?你也没定义呀
methods: { handleClick() { console.log(this) // vue实例 }}复制代码
运行的时候,它会帮你自动找到上下文的~