作用域(静态分析)
作用域是定义变量和访问变量的范围。
作用域可以通过静态分析,不需要运行代码,就可以分析出当前作用域。
作用域分为:全局作用域、函数作用域、块级作用域。
- 全局作用域:在顶层声明的变量和函数可以被访问的范围,通常就是在最外层(非函数体或循环体内)。全局变量中声明的变量和函数在任何地方都可以访问,包括其他作用域内部。在全局作用域中声明的变量,称为全局变量。
- 函数作用域:函数作用域是在函数体内部声明的变量可以被访问的范围。在函数体外部无法被访问。函数作用域可以创建私有变量,这些被声明的变量只能在函数体中被访问。
- 块级作用域:块级作用域是在代码块(if条件语句、for循环或大括号{}中的代码)内(let、const)声明的变量可以被访问的范围。在ES6(ECMAScript2015)之前,JavaScript中没有明确的块级作用域。可以使用var声明函数作用域变量。在ES6中引入let和const关键字后,当使用let和const声明变量时会产生块级作用域,变量的可访问范围在当前代码块内。
作用域规定了标识符(变量、函数等)在程序中的可见性和生命周期。当变量被引用时,JavaScript引擎会根据作用域找到对应的变量。如果当前作用域中找不到该变量,就会继续往上级作用域中查找,直到找到或者到达全局作用域。这就形成了作用域链(这个过程被称为作用域链)(scope chain)。
作用域链
作用域链是变量和函数的可访问性和查找规则。
function outer() {
const outerVar = 'Outer variable';
function inner() {
const innerVar = 'Inner variable';
console.log(innerVar); // Inner variable
console.log(outerVar); // Outer variable
console.log(globalVar); // Global variable
}
inner();
}
const globalVar = 'Global variable';
outer();
从上述代码可以看到,内部函数可以访问自身函数体内的变量,又可以访问外部函数体的变量,还能访问全局变量。作用域链会从内层,到外层,逐层查找。
如果不使用var声明变量呢?
console.log(d) //报错:is not defined // 此时并没有发生变量提升
d=yunyin
// 'yunyin'
window.d
// 'yunyin'
function test() {
e ='yy'
}
test()
console.log(e) // yy
console.log(window.e) // yy
可以看到,如果不用var进行变量声明,只做赋值操作,此时的变量会被挂在在window对象上。因此只有当变量使用var声明之后才会产生变量提升。
函数提升
函数提升是默认存在的,目的是为了方便开发者,可以随意放置函数的位置。
变量提升
只有使用var声明的变量才会产生变量提升,只会提升变量定义,不会提升变量赋值。因此在变量声明之前使用var声明的变量会返回undefined(变量提升)。如果使用let、const声明的变量,在变量声明之前访问,会报错 Cannot access 'xx' before initialization。报错提示,无法在变量初始化之前访问。如:
console.log(dog)
let dog = 'wangcai'
如果是在函数中定义的变量在外部访问,则会报错 Uncaught ReferenceError: dog is not defined。如下:
function funLog() {
let dog = 'wangcai'
}
console.log(dog)
可以看到报错内容是不一样的。
this上下文(动态分析)
this是在执行时动态读取上下文决定的,而不是创建时决定的
函数直接调用——this指向window
function foo(){
console.log('函数内部', this) //window对象
}
foo()
从上述代码可以看到,在函数体内的this指向window对象。这是因为当前被调用的foo函数,是在全局环境下被调用的。
隐式绑定——this执行调用堆栈的上级(对象、数组等引用关系逻辑)
function fn() {
console.log('隐式绑定', this) //obj对象
}
const obj = {
a: 1,
fn
}
obj.fn = fn;
obj.fn();
从上述代码可以看出,当函数被obj对象引用后,obj对象调用函数时,this执行的是obj对象。说明当前函数作用域被调用时,指向被调用的对象。注意,引用不代表执行,this指向的是执行的对象,而不是引用对象。(当前函数是在哪里执行的)
猜猜this指向
const foo = {
bar: 10,
fn: function () {
console.log(this.bar)
console.log(this)
}
}
const fn1 = foo.fn
fn1()
上述代码中,定义了fn1单独取出foo对象中的fn方法,此时定义的函数在windows上,因此this的上下文指向的是window,window上没有bar变量,所以为undefined
const o1 = {
txt: 'o1',
fn: function (){
//直接使用上下文——传统派活
console.log('o1fn', this)
return this.txt
}
}
const o2 = {
txt: 'o2',
fn: function (){
//呼叫领导执行——部门协作
return o1.fn()
}
}
const o3 = {
txt: 'o3',
fn: function (){
//直接内部构造——公共人
let fn = o1.fn
return fn()
}
}
console.log('o1fn', o1.fn());
console.log('o2fn', o2.fn());
console.log('o3fn', o3.fn());
o1调用fn,此时this指向o1,在o2中只是返回了o1的调用,依然属于o1自身的调用,因此this是o1,o3把o1.fn重新抽出来赋给o3对象中的fn,返回定义的fn函数,此时调用fn与o3之间并无关联,fn是在window上执行的,而window上并没有text的值,因此返回undefined
如果需要将console.log('o2fn', o2.fn()) 的结果改成o2,如下:
//在o2初始化fn方法时,把o1的方法抽出,赋给o2方法
const o1 = {
txt: 'o1',
fn: function (){
//直接使用上下文——传统派活
console.log('o1fn', this)
return this.txt
}
}
const o2 = {
txt: 'o2',
fn: o1.fn
}
console.log('o2', o2.fn()) //this指向o2
显示绑定this
function foo() {
console.log('函数内部', this);
}
foo();
// 使用
foo.call({
a: 1
});
foo.apply({
a: 1
});
const bindFoo = foo.bind({
a: 1
});
bindFoo();
foo()属于函数调用此时this指向window,call、apply、bind调用后都让this指向了对象{a: 1}
call、apply、bind区别
(1)执行时机
- call接收多个参数,第一个参数是this指向,从第二个参数开始是传递给函数的参数,call会立即执行函数
- apply接收两个参数,第一个参数是this指向,第二个参数是一个数组,数组中的元素会被展开后传递给函数,apply也会立即执行函数
- bind接收多个参数,第一个参数是this指向,第二个参数开始是传递给函数的参数。与call和apply不同,bind不会立即执行函数,而是返回一个新的函数,这个新的函数在调用时才会执行。
(2)参数传递方式
- call和apply都可以传递数组或伪数组对象作为参数
- apply的第二个参数必须是数组或伪数组,而call和bind的参数是独立传递的
(3)使用场景
- call适用于动态传递参数给函数的场景,特别是在不知道具体参数数量时
- apply常用于需要传递多个参数给函数的场景,特别是参数较多时,使用apply可以避免手动展开参数的麻烦
- bind适用于需要预先绑定this和部分参数,但不想立即执行函数的场景。bind返回的新函数可以在需要时调用,且可以保存绑定状态,直到实际需要执行时才使用。
bind实现
//1.需求:手写bind => bind位置(挂载在哪里) => Funtion.prototype
Function.prototype.newBind = function(){
const _this = this
//2.bind是什么?
const args = Array.prototype.slice.call(arguments) //把伪数组转成数组
// const args = [...arguments].slice(1) //把伪数组转成数组,并取出第一项
// 输入:args特点,第一项是新this,第二项~最后一项函数传参
const newThis = args.shift() //取出数组中的第一项
// 返回:返回的是一个函数 =>构造一个函数 =>这个函数返回原函数的结果且继承传参
return _this.newApply(newThis, args)
}
Function.prototype.newApply = function(context){
//边缘检测
if (typeof this !== 'function') {
throw new TypeError('使用正确的函数进行调用') //如果当前调用方不是函数没法往下执行
}
//如果传入为空报错,context为新的上下文,如果没传默认在window上执行
context = context || window
// 执行函数的替换
context.fn = this //指向当前执行的对象
// 临时挂载指向fn => 销毁临时挂载
let result = arguments[1]
? context.fn(...arguments[1])
: context.fn()
delete context.fn
//返回结果
return result
}
闭包
function mail(){
let content = 'mail'
return function(){
console.log(content)
}
}
const envelop = mail()
envelop()
//局部变量content逃逸到了外部
闭包的含义:函数内部作用域以函数包裹的形式传递到外部,使内部作用域的变量逃逸到外部。
函数可以使JS产生封闭作用域,这JS模块的基石
闭包可以使模块返回变量,让JS真正实现模块化