目录
- 1、apply/call/bind
- 2、作用域、作用域链和闭包
- 核心
- 1、预处理(解析阶段)——JS执行“代码段”之前
- 2、生成执行上下文环境——对代码段(全局/函数体)进行处理
- 3、执行上下文环境小结
- 4、多个执行上下文环境
- 5、作用域
- 6、作用域和执行上下文
- 7、从【自由变量】到【作用域链】
- 8、闭包(基础)
- 8.1 什么是闭包
- 8.2 闭包的问题:会引起内存泄漏
- 9、推荐:上下文,作用域和闭包文章
- 3、this指向
- 1、确认时机
- 普通函数--this--确定时机(`调用`)
- 箭头函数--this--确定时机(`定义`)
- 2、JS的this指向的四种情况
- 个人理解
- 2.1、构造函数—>指向new出的对象
- 2.1.2 原型对象—>指向调用的函数对象
- 2.2、函数作为对象的一个属性—>指向该对象
- 2.3、函数用call或者apply调用—>指向传入对象的值
- 2.4、全局 & 调用普通函数—>指向window对象
- 3、箭头函数的this指向
- 4、Vue的this指向(组件中this大多是指向组件实例)
1、apply/call/bind
共同点:
- apply()、call() 和 bind() 方法 三者作用都是 改变this指向。
- 接收的第一个参数都是this要指向的对象
区别:
- apply只有两个参数,第二个参数为数组;
- call和bind传参相同,多个参数依次传入的;
- call和apply都是对函数进行直接调用(立即执行),而bind方法不会立即调用函数,而是返回一个修改this后的函数(调用 修改后的函数)。
function testFn (param1, param2, param3) {
console.log(this, '-----this')
console.log(param1, param2, param3, '----------->param1, param2, param3')
}
testFn() // 指向window
const obj = {
type: 'this指向'
}
testFn.apply(obj, ['参1', '参2']) // 指向obj
testFn.call(obj, '参1', '参2') // 指向obj
const newFn = testFn.bind(obj, '参1', '参2') // '参3'也可以写在这
newFn('参3') // '参3'---> param3 // 指向obj
// 二次调用
testFn() // 指向window
newFn() // 指向obj
2、作用域、作用域链和闭包
核心
- 作用域:也叫执行上下文环境
- 作用域:只有全局作用域、函数体代码段(函数作用域)----ES5之前
- 作用域中
变量的值
是在执行
过程中产生的(根据作用域上下文查找), 作用域
是在函数创建
时就确定了(★★★★★★★非常重要★★★★★★★★
)。- 作用域别想成this指向
1、预处理(解析阶段)——JS执行“代码段”之前
变量预解析(变量提升)
变量预解析就是指,把所有的变量声明提升到当前作用域的最前面,但不提升赋值操作。
例如:
console.log(a);
var a = 10;
上述代码的执行结果为undefined,它的实际执行顺序可以理解为:
var a;
console.log(a);
a = 10;
函数预解析(函数提升)
函数预解析则是把所有的函数声明提升到当前作用域的最前面,但不执行函数。
例如:
fn();
function fn(){ // 函数声明
console.log("a");
}
相当于:
function fn(){
console.log("a");
}
fn();
而:
a();
var a=function(){ // 函数表达式
console.log("a");
}
上述函数执行时则会报错,a is not a function
上述函数可以理解为:
var a;
a();
a=function(){
console.log("a");
}
2、生成执行上下文环境——对代码段(全局/函数体)进行处理
全局代码段
1.变量、函数表达式 —— 变量声明,默认赋值为undefined;
2.this —— 赋值(window);
3.函数声明 —— 赋值。
函数体代码段
1.变量、函数表达式 —— 变量声明,默认赋值为undefined;
2.this —— 赋值(参考this指向);
3.函数声明 —— 赋值;
4.参数 —— 赋值;
5.argument —— 赋值;
6.自由变量的取值作用域 —— 赋值。
注意:
函数每被调用一次,都会产生一个新的执行上下文环境
函数在定义的时候
(不是调用的时候),就已经确定了函数体内部自由变量的作用域
。
这里要跟this指向区分开。(这里说的作用域先简单理解为,只是变量的作用域,跟this没关系)
3、执行上下文环境小结
执行上下文
的定义可以通俗化为 —— 在执行代码段之前(预处理阶段),把将要用到的所有变量都事先拿出来,有的直接赋值,有的先用undefined占个空
,这些变量共同组成的词法环境,即为执行上下文环境
。
4、多个执行上下文环境
处于活动状态的执行上下文环境只有一个。
其实这是一个压栈出栈的过程——执行上下文栈。
5、作用域
作用域是一个很抽象的概念,类似于一个“地盘”
如上图,全局代码和fn、bar两个函数都会形成一个作用域。
而且,作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。
例如,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
6、作用域和执行上下文
执行过程:
第一步,在加载程序时,已经确定了全局上下文环境,并随着程序的执行而对变量就行赋值。
第二步,程序执行到第27行,调用fn(10),此时生成此次调用fn函数时的上下文环境,压栈,并将此上下文环境设置为活动状态。
第三步,执行到第23行时,调用bar(100),生成此次调用的上下文环境,压栈,并设置为活动状态。
第四步,执行完第23行,bar(100)调用完成。则bar(100)上下文环境被销毁。接着执行第24行,调用bar(200),则又生成bar(200)的上下文环境,压栈,设置为活动状态。
第五步,执行完第24行,则bar(200)调用结束,其上下文环境被销毁。此时会回到fn(10)上下文环境,变为活动状态。
第六步,执行完第27行代码,fn(10)执行完成之后,fn(10)上下文环境被销毁,全局上下文环境又回到活动状态。
总结:
作用域只是一个“地盘”,一个抽象的概念,其中没有变量。
要通过作用域对应的执行上下文环境来获取变量的值。
同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。
所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。
7、从【自由变量】到【作用域链】
自由变量:
在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量。如下图
var x = 10,
function fn() {
var b = 20;
console.log(x + b); 这里的x就是一个自由变量
}
以上代码,在调用fn()函数时,函数体中第6行。取b的值就直接可以在fn作用域中取,因为b就是在这里定义的。
而取x的值时,就需要到另一个作用域中取。到哪个作用域中取呢?
要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”,切记切记
,如下图示例:
var x = 10,
function fn() {
console.log(x);
}
function show(f) {
var x = 20;
(function () {
f(); -------> 输出的是10,而不是20
})();
}
show(fn)
自由变量的作用域
取自由变量x的值时,要到哪个作用域中取?
————要到创建fn函数(包含了自由变量x)的那个作用域中取
——无论fn函数将在哪里调用。
作用域链
上面描述的只是跨一步作用域去寻找。
如果跨了一步,还没找到呢?——接着跨!——一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。
这个一步一步“跨”的路线,我们称之为——
作用域链
。
案例:理解作用域取值
第13行,fn()返回的是bar函数,赋值给x。
执行x(),即执行bar函数代码。
取b的值时,直接在fn作用域取出。
取a的值时,试图在fn作用域取,但是取不到,只能转向创建fn的那个作用域中去查找,结果找到了。
8、闭包(基础)
8.1 什么是闭包
var name = 'zs'
function foo(){
var message = 'Hello JavaScript!'
console.log(message, name)
}
foo()
这就是一个闭包的应用体现。
闭包:它储存了一个函数和一个关联的环境(上下文环境)
。
闭包和函数的最大的区别:,当捕获闭包的时候,它的自由变量会在捕获时被确定,这样即使脱离了捕获时的上下文,它也能照常运行。
js的闭包是通过作用域链实现的。并且每当创建一个函数,就会创建一个闭包。
闭包的个人理解
一个普通的function函数,如果它可以访问外层作用域中的自由变量,name这个函数就是一个闭包。
广义来讲:JavaScript中的函数都是闭包
狭义上讲:JavaScript中一个函数,如果访问了外层作用域的变量
,那么它一定是一个闭包。
8.2 闭包的问题:会引起内存泄漏
注意
记得变量的初始化处理,手动释放闭包产生的内存数据。
9、推荐:上下文,作用域和闭包文章
参考链接:https://www.cnblogs.com/wangfupeng1988/p/3977924.html
3、this指向
1、确认时机
普通函数–this–确定时机(调用
)
(1)在函数中this到底取何值,是在函数真正被调用执行的时候确定的
,函数定义的时候确定不了。
箭头函数–this–确定时机(定义
)
(1)箭头函数没有自己的this, 它的this是继承而来;
(2)默认指向在定义它时父级作用域(函数作用域)的this
,而不是执行时的对象。也可理解为:默认指向最近作用域( 【函数作用域】/【代码块】)中的this
(2.1)最近作用域:可以理解为是函数作用域,或者是代码块。注意,对象的{}
不是代码块。
(3)所谓父级
,可以理解为:箭头函数外层的打印console.log(this)
,这个this
指向谁就是谁
(4)父级作用域
准确来讲是父级的函数作用域
demo案例
const message = '全局message'
const obj = {
message: 'obj的message',
foo: () => {
// const message = 'foo的message'
const bar = () => {
console.log('bar', message) // 当foo函数里没有message定义时,会打印const message = '全局message'
console.log('bar', obj.message) // 只有这样才能拿到obj里的message
console.log('bar', this)
}
return bar
}
}
const fn = obj.foo()
fn()
2、JS的this指向的四种情况
个人理解
首先:this一定要指向一个对象。
其次:最顶层的对象,在浏览器环境中指的是window对象
然后:所有属性或者方法,都需要挂靠到对象下,即都是通过:对象.属性,对象.方法
这种方式使用
最后:执行
时,如果没有对象,那就算到window这个顶层对象上
2.1、构造函数—>指向new出的对象
function Foo() {
this.bar = '构造函数this指向new出来的对象'
console.log(this); // this指向new出来的对象
}
let f1 = new Foo()
console.log(f1,'f1对象');
console.log(f1.bar = 'new出来的对象');
2.1.2 原型对象—>指向调用的函数对象
function foo(name, age) {
console.log(this, name, age)
}
Function.prototype.hyapply = function (thisArg) {
console.log(this) // -> 当前调用的**函数对象**,即:foo函数
}
// Function.prototype是原型对象,原型对象也是对象
// Function.prototype.hyapply 就是对象里的一个方法
// foo可以看成是new Function构造函数出来的
// 因此调用时,this--->new出来的对象: foo
foo.hyapply()
2.2、函数作为对象的一个属性—>指向该对象
var obj = {
x:10,
fn:function () {
console.log(this); // this指向obj对象
function f() { // 函数f虽然在obj.fn内部定义的,但是它仍是个普通函数
console.log(this); // this指向window-------->情况特殊
}
f()
}
}
obj.fn()
console.log(obj);
2.3、函数用call或者apply调用—>指向传入对象的值
var obj = { x:10 }
var fn = function () {
console.log(this); // this指向obj对象
}
fn.call(obj)
2.4、全局 & 调用普通函数—>指向window对象
function Foo() {
console.log(this); // this指向window对象
}
var boo = function(){
console.log(this) // this指向window对象
}
boo()
Foo()
3、箭头函数的this指向
遇到箭头函数,直接
在箭头函数的外层
,打印console.log(this)
,看外层打印的this
指向谁即可
let obj = {
console.log(this) // 指向window
bbb:()=>{
console.log(this) // flag0:定义时判断,取父级的this指向,对象中this->window
},
aaa:function(){
console.log(this) // flag1:执行时判断,最近一层的对象是obj,this->obj
setTimeout(function(){
console.log(this) // flag2:执行时判断,最近一层无对象,this->window
setTimeout(function(){
console.log(this); // flag3:执行时判断,最近一层无对象,this->window
})
setTimeout(()=>{
console.log(this) // flag4:定义时判断,取父级的this指向,是flag2处的this->window
})
})
setTimeout(()=>{
console.log(this) // flag5:定义时判断,取父级的this指向,是flag1处的this->obj
setTimeout(()=>{
console.log(this); //flag6:定义时判断,取父级的this指向,是flag5处的this->obj
})
})
}
}
obj.aaa()
4、Vue的this指向(组件中this大多是指向组件实例)
组件中data
的this指向当前的组件实例对象
,无论是return前后
- main.js中
new Vue()
中,data的this指向undefined
所有的生命周期函数
中的this都指向vue实例对象[组件实例对象]
- vue的v-on指令中可以接收JS语句,其中的this是window(vue组件中的v-on指令除外)
computed
中的this指向vue实例对象[组件实例对象]
methods
中的this有以下几种情况
普通函数
的this指向vue实例对象[组件实例对象]
箭头函数
没有自己的this,因此this指向其宿主对象的this
(注意宿主对象是函数对象(它被调用后this的指向要进行具体分析),简单对象没有this)- 普通函数形式的回调函数的this是window,箭头函数形式的回调函数的this遵循箭头函数的原则(大多数情况下是vue实例对象)
// test.vue
<script>
export default {
data () {
let data1 = this // 组件实例: test.vue
console.log(data1, '---->return前的this');
return {
data2: this, // 组件实例: test.vue
data3: '',
}
},
computed: {
data4 () {
console.log(this, '---->computed的this'); //Vue实例(组件实例: test.vue)
return this
}
},
created: function () {
console.log(this, '---->所有生命周期里的this') //使用它的Vue实例(组件实例: test.vue)
this.data3 = this
this.showMessage1();
this.showMessage2();
this.showMessage3();
},
methods: {
// 普通函数
showMessage3 () {
console.log('普通函数........................指向vue实例(组件实例)');
console.log(this) //vue实例调用了showMessage3方法,this自然指向vue实例(组件实例: test.vue)
},
// 箭头函数
showMessage2: () => {
console.log('箭头函数........................指向undefined');
console.log(this); // this---->undefined
},
showMessage1 () {
setTimeout(function () {
console.log('函数未调用........................指向window');
console.log(this) //指向window 因为没人调用这个普通函数
}, 10)
}
}
}
</script>