目录
1、GO AO VO
1.1、函数里面的VO
叫AO
,全局的VO
叫GO
(早期的叫法,ES5之前,不包含ES5):在源代码里面声明的变量和函数,它们是作为VO
的一个属性添加上去的
1.2、新的ECMA规范里面改为 Variable Environment
GE
AE
VE
:在执行代码中声明的变量和函数,它们是作为一条环境记录(Environment Record
)添加到变量环境(VE
)的
2、代码解析
2.1、代码被解析时(解析的是函数、变量的定义语句,而不是函数的执行、或者变量的赋值语句),v8引擎内部会帮助我们创建一个对象(·GlobalObject -> GO·)。·GO·是在执行所有代码之前就会被提前创建好的。
2.2、在函数被解析时,会生成一个函数对象,这个对象包含了两个东西。一个是函数的父级作用域(parentScope
),一个是函数的执行体。因此,函数的父级作用域跟它的定义位置有关,跟它的调用位置无关。
var message = 'hello global';
// 定义在全局
function foo() {
// 在foo函数自己的内部作用域找message属性,没找到;然后在foo的父级作用域找,父级作用域为GO,结果就打印为"hello global"
console.log(message);
}
function bar(){
var meaasge = 'hello bar';
// 调用在局部
foo();
}
bar();
执行结果打印为:hello global
2.3、解析代码时干的事情:
把变量message
作为属性挂载到GO
上,注意这里代码还没执行,message
的值为undefined
;
把函数foo
做为属性挂载到GO
上,并会生成一个函数对象,这个对象包含了两个东西。一个是函数的父级作用域(parentScope
),一个是函数的执行体,注意这里还没有执行foo()
这条代码。
var message = 'hello world';
function foo() {
console.log('hello foo');
}
foo();
3、执行代码
3.1、v8为了执行代码,v8引擎内部会有一个 执行上下文栈(Execution Context Stack, ECStack
)(函数调用栈)。
3.2、因为我们执行的是全局代码,为了全局代码能够正常的执行,需要创建 全局执行上下文(Global Execution Context
)(全局代码需要被执行时才会创建)。
·
3.3、执行全局代码时,会创建一个全局执行上下文栈,v8引擎内部会帮助我们创建一个对象VO
,这里VO
指向GO
。
3.4、执行函数时,v8引擎内部会帮助我们创建一个对象VO
,这时VO
指向AO
(对应的函数的局部作用域,保存函数里面定义的变量,即为临时变量)。函数内部的代码执行完后,函数就会弹出栈(即函数对应的执行上下文栈就会销毁),执行函数时创建的AO
对象也会随之销毁。
4、在js中,函数是一等公民
表示函数的使用是非常灵活的。函数可以作为其他函数的返回值或者参数。js语法允许函数内部再定义函数。引出闭包。
5、闭包内存泄漏
已经没有使用的数据在内存里面没有被销毁调,就称之为内存泄漏。
function foo() {
var name = 'foo';
var age = 18;
function bar() {
console.log(name);
// 从js引擎优化的角度来说,虽然foo函数的AO对象由于闭包的原因没有销毁,但是AO对象上的age属性实际是被清空释放了的,因为bar函数里面没有用到age属性
// console.log(age);
}
return bar;
}
var fn = foo();
fn();
这里就会存在内存泄漏。原因:foo
函数已经执行完了,但foo
对应的AO
对象和bar
函数解析时生成的函数对象还存在在内存中。
原因是全局的GO
对象上的fn
属性还保留了对bar
函数对象的引用,而bar
函数对象里保存的parentScope
又指向foo
的AO
对象,foo
的AO
对象里的bar
属性又指向bar
函数对象。
解决方式:
var fn = null;
原因:取消掉GO
上fn
属性对bar
函数对象的引用,这样下一次垃圾回收机制开始的时候,由于从根对象GO
上没有路径指向bar
函数对象,那么就算bar
函数对象和foo
的AO
对象还是在互相引用,但也会被垃圾回收机制回收。
6、this的指向
6.1、在全局作用域下
6.1.1、浏览器:指向window(globalObject)
6.1.2、node环境:指向{}
6.2、函数里面的this是动态绑定的,是在执行的时候确定的。
函数里面的this
跟函数定义的位置无关,跟函数被调用时的方式以及调用的位置有关。
6.2.1、绑定规则一:默认绑定
默认绑定:独立函数调用 -> 指向window
例一:
function foo() {
console.log(this);
}
foo();
例二:
function foo1() {
console.log(this);
}
function foo2() {
console.log(this);
foo1();
}
function foo3() {
console.log(this);
foo2();
}
foo3();
例三:
var obj = {
name: 'Tom',
foo: function() {
console.log(this);
}
};
var fn = obj.foo;
fn(); // window
例四:
function foo() {
function bar() {
console.log(this);
}
}
var fn = foo();
fn();
6.2.2、绑定规则二:隐式绑定。
也就是它的调用位置中,是通过某个对象发起的函数调用。隐式绑定:object.fn()
-> object
对象会被js引擎绑定到fn
函数中的this
里面。
例一:
function foo() {
console.log(this);
}
var obj = {
name: 'Tom',
foo: foo
};
obj.foo(); // 指向obj对象
例二:
var obj1 = {
name: 'obj1',
foo: function() {
console.log(this);
}
};
var obj2 = {
name: 'obj2',
bar: obj1.foo
};
obj2.bar(); // 指向obj2
6.2.3、绑定规则三:显式绑定。
apply/call/bind
可以指定this
的绑定对象。
6.2.3.1、call/apply
function foo() {
console.log(this);
}
var obj = {
name: 'obj'
};
foo.call(obj); // 指向obj
foo.apply(obj); // 指向obj
foo.apply('aaa'); // 指向String构造函数
6.2.3.2、call/apply的区别:传参方式不同
function sum(num1, num2, num3) {
console.log(num1 + num2 + num3);
}
sum.call(null, 1, 2, 3); // 多个参数逗号拼接传递
sum.apply(null, [1, 2, 3]); // 多个参数通过数组传递
6.2.3.3、bind
function foo() {
console.log(this);
}
var newFoo = foo.bind('aaa');
newFoo(); // 指向aaa
默认绑定跟显示绑定bind冲突:显示绑定优先级大于默认绑定。
6.2.4、new绑定
当我们通过new
来调用一个函数的时候,它会自动帮我们生成一个对象。生成这个对象后,this
就会指向这个对象。当我们在函数内向this
添加属性的时候,就会默认把这个属性添加到生成的这个对象上。如果函数没有返回其他对象(用{}包起来的对象或者数组
,意思是就算显式的返回如字符串
、null
等值,打印p1还是默认返回的那个对象),那么函数就会默认返回这个生成的对象。
function Person(name, age) {
this.name = name;
this.age = age;
}
var p1 = new Person('tom', 18);
console.log(p1.name, p1.age);
7、this其他补充
7.1、浏览器内部的一些默认绑定
一般情况下,调用setTimeout
或者setInterval
定时器方法时,定时器的回调函数里的this
是一定指向window
的。
setTimeout(function() {
console.log(this); // window
}, 100);
相当于v8引擎内部实现setTimeout
方法时,调用它的回调函数时是直接调用的,即默认绑定
。
// 伪代码
setTimeout(fn, duration) {
fn(); // 默认绑定
}
7.2、浏览器内部的一些隐式绑定
<div class='box'>111</div>
var boxDom = document.querySelector('.box');
boxDom.onclick = function() {
console.log(this); // 指向boxDom这个对象
}
相当于v8引擎内部实现中,调用div绑定的点击方法是通过obj.fn()
的形式调用的,即隐式绑定
。
// 伪代码
boxDom.onClick();
7.3、浏览器内部的一些显式绑定
<div class='box'>111</div>
var boxDom = document.querySelector('.box');
boxDom.addEventListener('click', function() {
console.log(this); // 指向boxDom这个对象
});
相当于v8引擎内部实现中,调用div监听的点击方法是通过fn.call(boxDom)
的形式调用的,即显式绑定
。
7.4、数组:forEach/map/filter/find等 里面的this
默认情况下,数组方法的第一个参数回调函数里面的this
指向window
。
var names = ['aaa', 'bbb', 'ccc'];
names.forEach(function(item) {
console.log(item, this); // window
});
可以给数组方法传入第二个参数(有第二个参数的情况下,thisArg?
),改变数组方法的第一个参数回调函数里面的this
指向,相当于显式绑定
。
var names = ['aaa', 'bbb', 'ccc'];
names.forEach(function(item) {
console.log(item, this); // String{'aaa'}
}, 'aaa');
8、this绑定规则优先级
学习了四条规则,接下来我们只需要去查找函数的调用应用了哪条规则即可,但是如果一个函数调用位置应用了多条规则,优先级谁更高呢?
8.1、 默认绑定的优先级最低
8.2、显式绑定优先级 > 隐式绑定优先级
apply()
/call()
绑定测试:
var obj = {
name: 'obj',
foo: function() {
console.log(this)
}
}
obj.foo(); // 隐式绑定,this指向obj对象
obj.foo.call('aaa'); // 隐式绑定和显式绑定同时存在,this指向String{'aaa'}
bind()
绑定测试:
function foo() {
console.log(this);
}
var obj = {
name: 'obj',
foo: foo.bind('aaa')
}
obj.foo(); // this指向String{'aaa'}
8.3、new绑定优先级 > 隐式绑定优先级
var obj = {
name: 'obj',
foo: function() {
console.log(this);
}
}
var f = new obj.foo(); // this指向foo{}这个函数对象
8.4、new绑定优先级 > 显式绑定优先级
结论:new
关键字不能和apply
/call
一起使用。
所以,这里通过new和bind来测试:
function foo() {
console.log(this);
}
var bar = foo.bind('aaa');
var obj = new bar(); // this指向foo{}这个函数对象
8.5、结论
new
绑定 > 显式绑定(apply
/call
/bind
) > 隐式绑定(obj.fn()
) > 默认绑定(独立函数调用)
9、特殊绑定
9.1、忽略显式绑定
apply
/call
/bind
:当传入null
/undefined
时,自动将this
绑定成全局对象window
。
function foo() {
console.log(this);
}
foo.apply(null); // this指向window
foo.apply(undefined); // this指向window
var bar = foo.bind(null);
bar(); // this指向window
9.2、间接函数引用
var obj1 = {
name: 'obj1',
foo: function() {
console.log(this)
}
};
var obj2 = {
name: 'obj2'
}; // 注意这里一定要加分号,不然在进行js引擎在进行词法分析的时候,会认为当前语句还没结束,会报错
// 相当于js引擎会把
// {
// name: 'obj2'
// }(obj2.bar = obj1.foo)()当成是一个整体
(obj2.bar = obj1.foo)(); // 先执行赋值表达式,再调用函数,指向window
注意,上面的代码跟(obj1.foo)()
是不一样的,这里(obj1.foo)()
等价于obj1.foo()
,属于隐式调用
10、箭头函数 arrow function
- 箭头函数不会绑定
this
、arguments
属性 - 箭头函数不能作为构造函数来使用(不能和
new
一起来使用,会抛出错误)
10.1、常见的简写方式
- 如果只有参数只有一个,
()
可以简写 - 如果函数执行体只有一行代码,那么
{}
也可以省略,并且它会默认将这行代码的执行结果作为返回值
var nums = [1, 2, 3];
nums.filter(item => item % 2 === 0);
- 如果一个箭头函数只有一行代码,并且返回一个对象,这个时候可以在
{}
外面包上一个()
var nums = [1, 2, 3];
var newArr = nums.map(item => ({
id: item
}))
10.2、箭头函数中的this指向
箭头函数不使用this
的四种标准规则(也就是不绑定this
,即箭头函数内部作用域找不到this
这个属性),而是根据外层作用域来决定this
:
var foo = () => {
console.log(this);
};
foo(); // window
var obj = { foo };
obj.foo(); // window
foo.call('abc'); // window
应用举例:
var obj = {
data: [],
getData: function() {
// 发送网络请求,将结果放到上面的data属性中
// 在箭头函数之前的解决方案
// var _this = this;
// setTimeout(function() {
// _this.data = ['a', 'b', 'c'];
// }, 200)
// 箭头函数之后
setTimeout(() => {
this.data = ['a', 'b', 'c'];
}, 200)
}
};
obj.getData();
11、上层作用域的理解
var obj = {
name:'obj',
foo: function() {
// 上层作用域是全局
}
}
function Student() {
this.foo = function() {
// 上层作用域是Student
}
}
注:在我们之前讲的所有课程中,只有两个地方会产生作用域。第一个是我们的全局作用域;第二个是函数作用域。对象是不产生作用域的!!!
12、this面试题
12.1、面试题一
var name = "window";
var person = {
name: "person",
sayName: function() {
console.log(this.name)
}
};
function sayName() {
var sss = person.sayName;
sss(); // window:独立函数调用
person.sayName(); // person:隐式调用
(person.sayName)(); // person:隐式调用
(b = person.sayName)(); // window: 赋值表达式(独立函数调用)
}
sayName();
12.2、面试题二
var name = "window";
var person1 = {
name: 'person1',
foo1: function() {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function() {
return function() {
console.log(this.name)
}
},
foo4: function() {
return () => {
console.log(this.name)
}
}
};
var person2 = {name: 'person2'};
person1.foo1(); // person1:隐式绑定
person1.foo1.call(person2); // person2:显示绑定优先级大于隐式绑定
person1.foo2(); // window:不绑定作用域,上层作用域是全局
person1.foo2.call(person2); // window:不绑定作用域,上层作用域是全局
person1.foo3()(); // window:独立函数调用
person1.foo3.call(person2)(); // window:独立函数调用
person1.foo3().call(person2); // person2:最终调用返回函数时,使用的是显示绑定
person1.foo4()(); // person1:箭头函数不绑定this,上层作用域this是person1
person1.foo4.call(person2)(); // person2:上层作用域被显示绑定了person2
person1.foo4().call(person2); // person1:上层找到person1
12.3、面试题三
var name = 'window';
function Person(name) {
this.name = name;
this.foo1 = function() {
console.log(this.name)
};
this.foo2 = () => console.log(this.name);
this.foo3 = function() {
return function() {
console.log(this.name);
}
};
this.foo4 = function() {
return () => {
console.log(this.name)
}
}
}
var person1 = new Person('person1');
var person2 = new Person('person2');
person1.foo1(); // person1:隐式绑定
person1.foo1.call(person2); // person2:显示绑定优先级大于隐式绑定
person1.foo2(); // person1:上层作用域中的this是person1
person1.foo2.call(person2); // person1:上层作用域中的this是person1
person1.foo3()(); // window:独立函数度调用
person1.foo3.call(person2)(); // window:独立函数调用
person1.foo3().call(person2); // person2:显示绑定
person1.foo4()(); // person1:箭头函数不绑定this,上层作用域是person1
person1.foo4.call(person2)(); // person2:箭头函数不绑定this,上层作用域被显示绑定为person2
person1.foo4().call(person2); // person1:箭头函数不绑定this,上层作用域是person1
12.4、面试题四
var name = 'window';
function Person(name) {
this.name = name;
this.obj = {
name: 'obj',
foo1: function() {
return function() {
console.log(this.name);
}
},
foo2: function() {
return () => {
console.log(this.name);
}
}
}
}
var person1 = new Person('person1');
var person2 = new Person('person2');
person1.obj.foo1()(); // window:独立函数调用
person1.obj.foo1.call(person2)(); // window:独立函数调用
person1.obj.foo1().call(person2); // person2:显示绑定
person1.obj.foo2()(); // obj:箭头函数不绑定this,上层作用域为隐式绑定的obj
person1.obj.foo2.call(person2)(); // person2:箭头函数不绑定this,上层作用域为显示绑定的person2
person1.obj.foo2().call(person2); // obj:箭头函数不绑定this,上层作用域为隐式绑定的obj
13、手动实现call/apply/bind方法
13.1、call()方法的实现
Function.prototype.hycall = function(thisArg, ...args) {
// 在这里可以去执行调用的那个函数(foo)
// 问题:怎样可以获取到是哪一个函数执行了hycall
// 1.获取需要被执行的函数(原理:hycall()被调用时是隐式绑定,hycall()内部的this就指向了.hycall()前面的那个对象)
var fn = this;
// 2.把thisArg转成对象类型(防止它传入的是非对象类型)
thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
// 3.调用需要被执行的函数
thisArg.fn = fn; // 把fn()函数作为一个属性绑定到我们传入的参数上
var result = thisArg.fn(...args); // 隐式绑定,把fn()内部的this指向传入的参数(thisArg)
delete thisArg.fn; // 调用完fn()函数后,如果不需要这个属性,可以删除掉
// 4. 将最终的结果返回出去
return result;
}
function foo() {
console.log('foo被调用了',this)
}
function sum(num1, num2) {
console.log('sum被调用了',this)
return num1 + num2;
}
foo.hycall('123');
sum.hycall({}, 1,2,3);
13.2、apply()方法的实现
Function.prototype.hyapply = function(thisArg, argArray) {
// 1.获取到要执行的函数
var fn = this;
// 2.处理绑定的thisArg
thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
// 3.调用需要被执行的函数
thisArg.fn = fn;
argArray = argArray || [];
var result = thisArg.fn(...argArray)
delete thisArg.fn
// 4.将最终结果返回去
return result
}
function foo() {
console.log('foo被调用', this)
}
function sum(num1, num2) {
console.log('sum被调用', this)
return num1 + num2
}
// 系统调用
// var result = sum.apply('abc', [20, 30])
// console.log(result)
// 自己实现的调用
foo.hyapply({});
var result = sum.hyapply('abc', [20, 30])
console.log(result)
13.3、bind()方法的实现
Function.prototype.hybind = function(thisArg, ...argArray) {
// 1.获取到要执行的函数
var fn = this;
// 2.处理绑定的thisArg
thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window
function proxyFn(...args) {
// 3.将函数放到thisArg中进行调用
thisArg.fn = fn;
// 特殊:对两个传入的参数进行合并
var finalArgs = [...argArray, ...args];
var result = thisArg.fn(...finalArgs)
delete thisArg.fn
return result
}
return proxyFn
}
function foo() {
console.log('foo被执行', this)
}
function sum(num1, num2, num3, num4) {
console.log(num1, num2, num3, num4)
}
// var bar = foo.hybind('abc')
// var result = bar()
// console.log(result)
var newSum = sum.hybind('abc', 10, 20)
var result = newSum(30, 40)
console.log(result)
14、arguments
14.1、arguments的基本使用
function foo(num1, num2, num3) {
// 类数组(array-like)对象(长的像是一个数组,本质上是一个对象):arguments
// console.log(arguments)
// 常见的对arguments的操作是三个
// 1.获取参数的长度
console.log(arguments.length)
// 2.根据索引值获取某一个参数
console.log(arguments[3])
// 3.callee获取当前arguments所在的函数
console.log(arguments.callee)
}
foo(10, 20, 30, 40, 50)
14.2、类数组转数组:Array.prototype.slice.call()
js内部slice
方法的实现:
Array.prototype.hyslice = function(start, end) {
var arr = this;
// ... 对start、end参数的一些处理
start = start || 0;
end = end || arr.length
var newArray = [];
for (var i = start; i< end; i++) {
newArray.push(arr[i])
}
return newArray
}
var newArray = Array.prototype.hyslice.call(['aaa', 'bbb', 'ccc'], 1, 3)
console.log(newArray)
14.3、实现:将arguments转成array
function foo(num1, num2, num3) {
// 1.Array.prototype.slice将arguments转成array
var newArr1 = Array.prototype.slice.call(arguments)
console.log(newArr1)
var newArr2 = [].slice.call(arguments)
console.log(newArr2)
// 2.ES6的语法
var newArr3 = Array.from(arguments)
console.log(newArr3)
var newArr4 = [...arguments]
console.log(newArr4)
}
foo(10, 20, 30, 40, 50)