作用域链
function bar() {
console.log(myName)
}
function foo() {
var myName = "阿春"
function bar2() {
console.log('2', myName);
}
bar();
bar2();
}
var myName = "阿春呀"
foo()
// 阿春呀
// 2 阿春
变量值的引用不是按照调用栈的顺序来查找变量。每个执行上下文的变量环境中都包含了一个外部引用outer,用来指向外部的执行上下文。上面的代码段中,bar和foo指向的都是全局上下文。这个查找的链条就是作用域链。
- 词法作用域:指作用域是由代码中函数声明的位置来决定的,是代码编译阶段就决定好的,和函数如何调用无关。
比如上面的代码块中,存在的词法作用域链为:bar() -> 全局作用域 | bar2() -> foo() -> 全局作用域
function bar() {
var myName = "bar"
let test1 = 100
if (1) {
let myName = "{}内}"
console.log(test)
}
}
function foo() {
var myName = "foo"
let test = 2
{
let test = 3
bar()
}
}
var myName = "window"
let myAge = 20
let test = 1
foo()
首先查找bar内的词法环境 myName/test1;接着查找函数变量环境 myName;接着随着outer查找全局执行上下文,首先看词法环境myAge,test。找到值,因此输出1。
闭包
- 闭包:在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
function foo() {
var myName = "极客时间"
let test1 = 1
const test2 = 2
var innerBar = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName("极客邦") // JavaScript 引擎会沿着“当前执行上下文–>foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量,
bar.getName()
console.log(bar.getName())
根据词法作用域,内部函数getName和setName总是可以访问它们的外部函数foo中的变量,所以当innerBar对象返回给全局变量bar时,虽然foo函数已经执行结束,但其中的test1和myName依然保存在内存中,这些变量除了bar,其他地方都无法访问,这个变量集Closure(foo)称为闭包。
-
闭包的回收:如果闭包一直使用,那么可以作为一个全局变量存在;如果使用频率不高,尽量成为一个局部变量。因为全局变量闭包会一直存在直到页面关闭,但如果这个闭包不再使用,会造成内存泄漏。
-
闭包的应用
// 闭包实现计数器
var add = (function(){
var counter = 0;
return function(){
return(++counter);
}
})();
- 极客时间课后题:
var bar = {
myName:"time.geekbang.com",
printName: function () {
console.log(this.myName)
}
}
function foo() {
let myName = "极客时间"
return bar.printName
}
let myName = "极客邦"
let _printName = foo()
_printName()
bar.printName()
不会形成闭包,输出为 极客邦 极客邦。因为JavaScript语言的作用域链是由词法作用域决定的,而词法作用域是由代码结构决定的。因此printName中使用的变量是全局作用域下的。
// 引入this
var bar = {
myName1:"time.geekbang.com",
printName: function () {
console.log(this.myName1)
}
}
function foo() {
let myName = "极客时间"
return bar.printName
}
let myName = "极客邦"
let _printName = foo()
_printName()// this指向window,输出极客邦
bar.printName()// this指向bar,
会形成闭包,_printName(): this指向window,输出极客邦;bar.printName(): this指向bar,time.geekbang.com。
this
因为JavaScript中也需要在对象内部的方法中使用对象内部的属性,因此存在this机制。
执行上下文
- 全局执行上下文:执行全局代码的时候编译并创建全局执行上下文,整个页面的生存周期内,只有一份;
- 函数执行上下文:调用函数的时候编译并创建,函数执行结束后,创建的函数执行上下文会被销毁;
- eval函数也会创建执行上下文。
this的指向
- 使用create创建对象时,this指向该对象(通过构造函数中设置)
function CreateObj(){
console.log(this);
this.name = "是阿春呀";
}
CreateObj(); // window
var myObj = new CreateObj(); // CreateObj
new操作时,首先创建一个空对象tempObj(var tempObj = {});接着调用CreateObj.call(tempObj)方法(CreateObj.call(tempObj));然后执行CreateObj函数(此时CreateObj的this指向tempObj对象);最后返回tempObj对象。(return tempObj)
- this的值不由函数定义放在哪个对象里决定,而是函数执行时由谁来唤起决定。(通过对象调用方法设置)
var person = {
name: "Jay",
greet: function() {
console.log("hello, " + this.name);
}
};
person.greet();
// hello, Jay
// person调用,this指向person
var greet = person.greet;
greet();
// hello,
// window调用,此时是undefined
- 特殊的箭头函数:箭头函数按词法作用域来绑定它的上下文,所以this实际上会引用到原来的上下文。
箭头函数保持它当前执行上下文的词法作用域不变,而普通函数则不会。换句话说,箭头函数从包含它的词法作用域中继承到了 this 的值。
var object = {
data: [1,2,3],
dataDouble: [1,2,3],
double: function() {
console.log("this inside of outerFn double()");
console.log(this);// 1
return this.data.map(function(item) {
console.log(this);// 2
return item * 2;
});
},
doubleArrow: function() {
console.log("this inside of outerFn doubleArrow()");
console.log(this);//3
return this.dataDouble.map(item => {
console.log(this);//4
return item * 2;
});
}
};
object.double(); // 1.object对象2.window对象(因为map是window下的函数)
object.doubleArrow(); // 3.object对象4.object对象
- 严格模式 use strict: 在严格模式下,最外层的this不指向window而是undefined
this指向练习题
var myObj = {
name : "是阿春呀",
showThis: function(){
var self = this;
console.log(this)// myObj
function bar(){
console.log(this); // window(因为this的绑定可以通过new运算符、call/apply/bind、通过对象调用等方式进行,但是不会通过作用域链绑定this,所以对于独立调用的函数,如果未进行有效的this绑定的话,this就会绑定到window对象或undefined)
console.log(self); // myObj(将this体系转换为了作用域体系)
}
bar()
}
}
var myObj = {
name : "是阿春呀",
showThis: function(){
console.log(this) // myObj
var bar = () => {console.log(this)} // myObj
bar()
}
}
myObj.showThis()
var x = 11;
var obb1 = {
x: 222,
y: {
x: 333,
obc: function f() {
console.log('a', this);
var x = 111;
var obj = {
x: 22,
say: () => {
console.log('b', this.x);
}
}
obj.say()
}
}
}
var obb2 = {
x: 222,
y: {
x: 333,
obc: function f() {
console.log('a', this);
var x = 111;
var obj = {
x: 22,
say: function f() {
console.log('b', this.x);
}
}
obj.say()
}
}
}
obb1.y.obc();
// a {x: 333, obc: ƒ} b 333
obb2.y.obc();
// a {x: 333, obc: ƒ} b 22
解析:=>继承之前的this作用域,不会新增作用域
function fn1()
{
this.user = '是阿春呀';
return {};
}
var a = new fn1;
function fn2()
{
this.user = '是阿春呀';
return 0;// 或者null,undefined
}
var b = new fn2;
console.log(a.user); //undefined
console.log(b.user); //是阿春呀
如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊
修改this指向
- bind: func.bind(this, param1, param2, …)
适用:参数固定,不立即执行
var bobObj = {
name: "Bob"
};
function print() {
return this.name;
}
// 将 this 明确指向 "bobObj"
var printNameBob = print.bind(bobObj);
console.log(printNameBob()); // this 会指向 bob,于是输出结果是 "Bob"
- call: print.call(this, param1, param2, …),并执行函数
适用:参数固定,立即执行,
var item = {
name: "I am"
};
function print() {
return this.name;
}
// 立刻执行
var printNameBob = console.log(print.call(item));
// 将 argument 对象转化成一个数组
function add (a, b) {
return a + b;
}
function sum() {
return Array.prototype.reduce.call(arguments, add);
}
console.log(sum(1,2,3,4)); // 10
- apply: func.apply(this, [params…])
Math.min.apply(null, [1,2,3,4]);
// 返回 1
console.log.apply(console, [1,3,'I am a string', {name: "jay", age: "1337"}, [4,5,6,7]]);
// 1 3 "I am a string" {name: "jay", age: "1337"} (4) [4, 5, 6, 7]
后续待学习
- 方法借用
- 柯里化
- 偏函数应用
- 依赖注入
参考资料
- 《深入理解JavaScript this》作者:老教授 链接:https://juejin.im/post/5aefe76e6fb9a07abc29d4a1 来源:掘金
- 《浏览器工作原理与实践》作者:李兵
https://time.geekbang.org/column/intro/100033601?tab=catalog 来源:极客时间