执行上下文
在讲作用域之前我们来说一个知识点:执行上下文。每一个函数都有自己的执行上下文EC(执行环境 execution context),并且每个执行上下文中都有它自己的变量对象VO(Variable object),用于存储执行上下文中的变量 、函数声明 、函数参数,这解释了js如何找到我们定义的函数和变量。并且函数是js中唯一一个能创建出作用域的,注意:for,if()else()不能创建作用域。看一段代码
console.log (a) // undefined
var a = 10
此时打开控制台就会打印undefined,这是因为在代码还没执行之前,a会被提前声明,此时的代码可以理解为
var a
console.log (a) // undefined
var a = 10
代码执行到console.log (a) 时a只声明了,并没有赋值,所以结果当然是undefined。再来看一段代码
fn ("zhangsan",20)
function fn (name) {
age = 20
console.log (name,age) // "zhangsan" 20
var age
}
此时打印的结果就是“zhangsan”,20。我们可以将代码理解为
function fn (name) {
var age
age = 20
console.log (name,age) // "zhangsan" 20
}
fn ("zhangsan",20)
一段或者一个函数都会生成执行上下文,针对一段会生成全局的执行上下文,在执行之前先将变量定义和函数声明提前,针对一个函数会生成一个函数执行上下文,在函数执行之前,将函数中的变量定义,函数声明,this,arguments拿出来。
要注意的是函数声明和函数表达式的区别,看代码
function fn () { } // 函数的声明
var fn = function () { } // 函数表达式
this
其实本质上来说,js里面的this是一个指向函数执行环境的指针
,this在哪个函数里面,就指向这个函数的context,this永远指向最后调用它的对象,并且this要在执行时才能确认值,定义时无法确认。 例如
var a = {
name : "A",
fn : function () {
console.log (this.name)
}
}
a.fn() // this === a
a.fn.call ({name : "B"}) // this === {name : "B"},
var fn1 = a.fn
fn1() // this === window
第一种情况,是a调用了fn(),所以此时的this为a;第二种情况,使用call(),将this的值指定为了{name : "B"};第三种情况,把a.fn赋给了fn1,但是调用时却是window在调用,所以this === window
this其实有好几种使用场景,在这里我们主要介绍其中四种
- 作为构造函数执行
function Foo (name,age) {
this.name = name // this === f
this.age = age // this === f
//return this
}
var f = new Foo ("zhangsan",20)
在这里new操作符会改变函数this的指向问题,指向创建的对象f。为什么this会指向f?首先new关键字会创建一个空的对象,然后会自动调用一个函数apply方法,将this指向这个空对象,这样的话函数内部的this就会被这个空的对象替代。
更新一个小问题:若构造函数中返回this或返回值是基本类型(number、string、boolean、null、undefined)的值,则返回新实例对象;若返回值是引用类型的值,则实际返回值为这个引用类型。例如:
function Fn()
{
this.user = 'Jay';
return {};
}
var a = new Fn();
console.log(a.user); //undefined
function Fn()
{
this.user = 'Jay';
return function () {};
}
var a = new Fn;
console.log(a.user); //undefined
function Fn()
{
this.user = 'Jay';
return 1;
}
var a = new Fn();
console.log(a.user); // Jay
还有一点就是虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊。
function Fn()
{
this.user = 'Jay';
return null;
}
var a = new Fn;
console.log(a.user); // Jay
知识点补充:在严格版中的默认的this不再是window,而是undefined
- 作为普通函数执行
function fn () {
console.log (this) // this === window
}
fn ()
- 作为对象属性执行
var obj = {
name : "A",
printName : function () {
console.log (this.name) // this === obj
}
}
obj.printName ()
- call() apply() bind()
这三种方法都可以改变函数体内部this的指向,但是又存在一些差异。下面,我们看一个例子让你完全搞懂这三个方法的异同点。
var name = "小明" , age = "17"
var obj = {
name : "安妮",
objAge :this.age,
fun : function () {
console.log ( this.name + "今年" + this.age )
}
}
console.log(obj.objAge) // 17
obj.fun() // 安妮今年undefined
出现这样的结果是因为,在obj.objAge中,this的指向是obj,但是这句话外部没有函数包着,所以这里的执行环境是全局的;在obj.fun()中,this的指向也是obj,但是obj中没有定义age,所以是undefined。再来看第二个例子:
var name1 = "Jay"
function show () {
console.log (this.name1)
}
show() // Jay
这个很明显this指向的是window,所以打印的结果就是Jay。
下面我们分别用这三个方法重新定义this,看看是什么结果
var name = "小明" , age = "17"
var obj = {
name : "安妮",
objAge :this.age,
fun : function () {
console.log ( this.name + "今年" + this.age )
}
}
var a = {
name : "Jay",
age : 23
}
obj.fun.call(a) // Jay今年23
obj.fun.apply(a) // Jay今年23
obj.fun.bind(a)() // Jay今年23
我们会发现这三个方法的结果都一致,只有bind返回的是一个新的函数,你必须调用它才会被执行。
再来对比一下传参的情况:
var name = "小明" , age = "17"
var obj = {
name : "安妮",
objAge :this.age,
fun : function (like,dislike) {
console.log (this.name + "今年" + this.age ,"喜欢吃" + like + "不喜欢吃" + dislike)
}
}
var a = {
name : "Jay",
age : 23
}
obj.fun.call(a,"苹果","香蕉") // Jay今年23 喜欢吃苹果不喜欢吃香蕉
obj.fun.apply(a,["苹果","香蕉"]) // Jay今年23 喜欢吃苹果不喜欢吃香蕉
obj.fun.bind(a,"苹果","香蕉")() // Jay今年23 喜欢吃苹果不喜欢吃香蕉
call,bind,apply 这三个函数的第一个参数都是 this 的指向对象,注意如果call和apply的第一个参数写的是null,那么this指向的是window对象;第二个参数有区别:① call的参数是直接放进去的,用逗号分隔;② apply的所有参数都必须放在一个数组里面传进去;③ bind除了返回是函数以外,它的参数和call一样。当然,三者的参数不限定是string类型,允许是各种类型,包括函数 、 object 等等。
call和apply都是改变上下文中的this并立即执行这个函数,bind方法可以让对应的函数想什么时候调就什么时候调用,并且可以将参数在执行的时候添加。
在非严格模式下,call和apply的第一个参数如果指定为null或undefined时,会自动指向全局对象。