这篇文章主要收录了js作用域的深入理解和this。
1 提升原理(早期ECMA)
1.1 作用域
在es6之前,只有这几种作用域:
- 全局作用域
- 函数作用域
- with语句形成的
为什么会出现变量\函数提升?
这与全局代码执行过程有关
1.2 全局代码执行过程
例如这段代码
var name1 = 20
var name2 = 30
var res = name1 + name2
1.代码被解析
v8引擎内部会帮助我们创建一个全局对象(GlobalObject->GO)
并且会将这些全局变量放到该全局对象中,还未进行赋值操作
var name1 = 20
var name2 = 30
var res = name1 + name2
var globalObject = {
String: '类',
Date: '类',
setTimeout: "函数"
window: this,
name1: undefined,
name2: undefined,
res: undefined
}
2.运行代码
为了执行代码,v8引擎内部会有一个执行上下文execute context stack(函数调用栈);
而我们执行的是全局代码,为了全局代码能够正常执行,需要创建全局执行上下文(Global Execution context)(全局代码需要被执行时才会被创建);
而全局执行上下文里面有个variable object(VO)指向的是GO;
开始执行代码;
作用域提升的原理就是这样:
name1、name2、res一开始被放进了全局对象GO,并且值都为undefined,此时代码还未执行(编译阶段);
然后执行代码,为他们分别赋值;
要是在执行代码之前前想获取他们的值,只会是undefined;
变量是这样提升的,那函数呢?
1.3 函数执行过程
var name = 'zsf'
foo(123)
function (num) {
console.log(m)
var m = 10
var n = 20
}
同样,解析到函数时,会在调用栈里创建一个函数执行上下文(FEC),FEC包含2部分:
- VO:指向AO
- 执行代码
然后将num、m、n变量放到AO对象中,并赋值为undefined
执行代码后才会给num、m、n赋值
在此之前打印会是undefined
1.4 作用域链
查找一个变量时,真实的查找路径是沿着作用域链找的
var name = 'zsf'
foo(123)
function (num) {
console.log(m)
var m = 10
var n = 20
console.log(name)//zsf
}
其实AO有两部分组成:
- AO本身
- 父级作用域(这里是GO)
当在AO里面找不name时,会去GO里找
父级作用域在编译时已经确定了,与定义时位置有关系,而与调用时位置没有关系
当函数里嵌套函数同理
var name = 'zsf'
foo(123)
function (num) {
console.log(m)
var m = 10
var n = 20
function bar() {
console.log(name)
}
bar()//zsf
}
函数执行完对应的FEC就会弹出栈
1.5 面试题
var n = 100
function () {
n = 200
}
console.log(n)//200
function foo () {
console.log(n)//undefined
var n = 200
console,log(n)//200
}
var n = 100
foo()
var a = 100
function () {
console.log(a)//undefined
return
var a = 10
}
//return是执行阶段的,编译阶段依然会有a:undefined放VO中
2 this
2.1 为什么需要this?
没有this,平常写代码很不方便,拷贝一个对象时,很多地方可能都需要修改
2.2 this的指向
与函数定义时位置无关,与函数调用时位置有关
function foo() {
console.log(this);
}
// 1.直接调用这函数
foo()//window对象
var obj = {
name: 'zsf',
fn: foo
}
// 2.创建一个对象,对象中的函数指向foo
obj.fn()//obj对象
// 3.apply调用
foo.apply('123')//String对象
2.3 绑定规则
默认绑定
独立函数调用,指向window
函数调用时没有调用主体
function foo() {
console.log(this)
}
foo()//window对象
var obj = {
name: 'zsf',
fn: function foo() {
console.log(this)
}
}
var bar = obj.fn
bar()//window,依然没有调用主题
隐式绑定
v8引擎绑定的
函数通过某个对象进行调用的,this绑定的就是该对象
前提条件:
- 必须在调用的对象内部有一个对函数的引用(比如一个属性)
- 如果没有这样的引用,在进行调用时,会报找不到该函数的错误
- 正是通过这个引用,间接的将this绑定到了这个对象上
var obj = {
name: 'zsf',
fn: function foo() {
console.log(this)
}
}
obj.fn()//obj对象
var obj1 = {
name: 'zsf',
fn: function foo() {
console.log(this)
}
}
var obj2 = {
name: 'obj2',
fn: obj1.fn
}
obj2.fn()
显式绑定
如果不希望在对象内部包含这个函数的引用,同时又希望在这个对象上进行强制调用,怎么做?
每个函数对象都有这2个方法
- call()
- apply()
- bind()
function foo() {
console.log("被调用了", this);
}
// 直接调用和call()/apply()调用的区别在于this绑定不同
// 直接调用this指向window
foo()
// call()/apply()调用会指定this绑定对象
var obj = {
name: 'obj'
}
foo.call(obj)//obj
foo.apply(obj)//obj
foo.apply("aaa")//aaa
直接调用和call()/apply()调用的区别在于this绑定不同:
- 直接调用this指向window
- call()/apply()调用会指定this绑定对象
call和apply的区别
传参方式不同,call接收多个参数是以逗号分开,而apply会将多个参数放数组里
function sum(num1, num2) {
console.log(num1 + num2, this)
}
foo.call('call', 20, 30)
foo.apply('qpply', [20, 30])
bind的显示绑定
function foo() {
console.log(this)
}
foo.call('aaa')
foo.call('aaa')
foo.call('aaa')
foo.call('aaa')
等价于
function foo() {
console.log(this)
}
// 隐式绑定和显式绑定冲突了,根据优先级,显式绑定
var newFoo = foo.bind('aaa')
newFoo()
newFoo()
newFoo()
newFoo()
bing绑定之后会生成一个新的函数返回
new绑定
js中的函数可以当做一个类的构造函数来使用,也就是使用new关键字
function Person(name, age) {
this.name = name
this.age = age
}
var p1 = new Person('zsf', 18)
console.log(p1)
通过一个new关键字调用函数时(构造器),这个时候this是在调用这个构造器时创建出来的,并且this = 创建出来的对象
2.4 规则之外
忽略显式绑定
function foo() {
console.log(this)
}
foo.call(null)
foo.call(undefined)
call、apply、bind当传入参数为null或undefined时,自动将this绑定到window对象
间接函数引用
var obj1 = {
name: 'obj1',
foo: function () {
console.log(this)
}
}
var obj2 = {
name: 'obj2'
};
(obj2.bar = obj1.foo)()//this
(obj1.foo)()//obj1
这属于独立函数调用(无等号就是隐式绑定)
箭头函数
基础语法
箭头函数不会绑定this、arguments属性
箭头函数不能作为构造函数来使用(不能和new关键字一起使用)
- ()
- =>
- {}
简写:
- 只有一个参数可以省()
- 只有一行执行提可以省{} ,并且会将该行代码当结果返回
- 执行体只有一行且返回的是一个对象,小括号()括起来
var nums = [1,2,3]
nums.forEach(item => {
console.log(item)
})
var nums = [1,2,3]
nums.filter(item => item % 2 === 0)
var bar = () => ({name: 'zsf', age: 18})
规则
箭头函数不绑定this,而是根据外层作用域来决定this
var foo = () => {
console.log(this)
}
foo()//window
var obj = {
fn: foo
}
obj.fn()//window
foo.call('abc')//window
这样有什么应用呢?
在箭头函数出来之前
var obj = {
data: [],
getDate: function () {
// 发送网络请求,将结果放上面的data属性中
var _this = this
setTimeout(function () {
var result = ['abc', 'bbv', 'ccc']
_this.data = result
}, 2000)
}
}
// 由于这里的隐式绑定,第5行的this绑定了obj对象。才有了,第8行的写法
obj.getDate()
箭头函数出来之后
var obj = {
data: [],
getDate: function () {
// 发送网络请求,将结果放上面的data属性中
setTimeout( () => {
var result = ['abc', 'bbv', 'ccc']
this.data = result
}, 2000)
}
}
obj.getDate()
箭头函数不绑定this,相当于没有this,会寻找上层作用域寻找this;
也就是在getData的作用域里找this,而obj.getData已经隐式绑定了getData里的this指向obj
2.5 一些函数的this分析
setTimeout
setTimeout(function () {
console.log(this)//window
}, 1000)
setTimeout内部使用的独立函数调用,所以this默认绑定window对象
2.6 规则优先级
- 默认最低
- 显式高于隐式
- new高于隐式
- new高于显式
var obj = {
name: 'obj',
fn: function foo() {
console.log(this)
}
}
obj.fn.call('abc')//abc
function foo() {
console.log(this)
}
var obj = {
name: 'obj',
fn: foo.bind('abc')
}
obj.fn()//abc
var obj = {
name: 'obj',
fn: function () {
console.log(this)
}
}
var p = new obj.fn()//fn函数对象
function foo() {
console.log(this)
}
var bar = foo.bind('aa')
var p = new bar()//foo
由于call和apply都是主动调用函数,所以不能和new一起使用
3 实现apply、call、bind
3.1 call
补充:
展开运算符... (类似遍历)
var names = ['abc', 'abb', 'ccc']
function foo (n1, n2, n3) {
}
foo(...names)
自己实现
// 给所有函数加上一个自定义call
Function.prototype.sfcall = function (thisArg, ...args) {
// 在这里可以执行调用者(函数)
// 问题1:如何获取到是哪个函数调用了sfcall?
var fn = this
// 边界情况edge case1 对thisArg转成对象类型(防止传入非对象类型报错)
// 边界情况edge case2 传入参数null/undefined
thisArg = thisArg ? Object(thisArg) : window
// 边界情况edge case2 调用者(函数)有一个或多个参数时
// 如何执行调用者(函数)?
thisArg.fn = fn
// 边界情况edge case3 调用者有返回值
var result = thisArg.fn(...args)
delete thisArg.fn
// 返回调用者的返回值
return result
}
function foo(n1, n2) {
console.log('foo执行了', this, n1, n2)
console.log(n1 + n2);
}
foo.sfcall('sss', 1, 2)
3.2 apply
有时间再补
3.3 bind
有时间再补
4 内存管理
4.1 内存泄漏
存在该释放内存空间没有回收
4.2 垃圾回收
(Garbage Collection)GC
不再使用的对象,都称之为垃圾,需要被回收
那GC怎么知道哪些对象不再使用呢?
GC算法
- 引用计数
- 标记清除
引用计数
每个对象会有个count 只要有引用指向,就+1;当count=0时,就回收该对象
但是这种算法会存在循环引用的问题(两个相互引用),count永不为0
标记清除
设置一个(root object)根对象,垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于那些没有引用到的对象,就认为是不可用对象
这种算法会解决循环引用问题
5 补充
5.1 with
with语句可以形成自己的作用域
var obj = {
name: 'zsf'
}
with(obj) {
console.log(name)
}
现在已经不推荐了
5.2 eval
全局函数
var str = 'console.log(123)'
eval(str)//123
不建议:
- 可读性差
- 可能会被修改内容,被攻击
- 必须经过js解释器,不能js引擎优化,执行效率低
5.3 严格模式
es5提出
严格模式的限制:
- 通过抛出错误来消除一些原有的静默(silent)错误
- 让js引擎执行代码时可以进行更多的优化
- 禁用了ECMAScript未来版本可能会定义的语法
开启
- js文件
- 某个函数中
js文件
文件顶部写上 use strict
某个函数中
function () {
'use strict'
...
}
严格模式常见限制
- 意外创建全局变量
- 不允许函数有相同参数名称
- 静默错误
- 不允许使用原先的8进制格式
- 不允许with语句
- eval函数不会向上引用变量
- 独立函数(自执行)的this指向undefined
- setTimeout的this指向依然是window