你不知道的JavaScript(核心知识点概念详细整理)前言 JavaScript 是一门灵活且功能强大的编程语言,它不
前言
JavaScript 是一门灵活且功能强大的编程语言,它不仅支撑着现代 Web 应用程序的前端开发,还通过 Node.js 在后端领域大放异彩。所以,作为JavaScript 开发者,需要深入理解其核心概念。本文将会由浅入深详细介绍作用域
、作用域链
、词法作用域
、预编译
、调用栈
、闭包
以及 this 关键字
、js的执行机制
,这些都是 JavaScript 中非常重要的概念。
正文
一、作用域(Scope)
作用域决定了变量在哪里可以被访问。JavaScript 中有两种主要的作用域:全局作用域和局部作用域(通常指函数作用域)。
全局作用域
- 全局作用域是最顶层的作用域,在浏览器环境中通常指
window
对象,它包含了所有未在任何函数内的变量和函数,全局作用域中的变量可以在任何
地方被访问。
var glob=2;
function foo() {
console.log(glob); // 可以访问,打印2
}
foo();
局部作用域
- 局部作用域通常指的是
函数作用域
,函数作用域是指在一个函数内部
定义的变量和函数只能在该函数内部被访问,反之则不行。
function func() {
var bar=2;
console.log(bar); // 可以访问,打印2
}
func();
console.log(bar); // 报错:bar is not defined
func
函数就形成了一个局部作用域。
块级作用域
- ES6引入了
let
和const
关键字,块级作用域是通过使用{}
来定义的一个区域,在这个区域内部声明的变量(使用let
或const
关键字)只在这个区域内有效
if (true) {
let a = 1
}
console.log(a); // 报错:a is not defined
let a = 1
if (1) {
console.log(a); // 暂时性死区 报错:Cannot access 'a' before initialization
let a = 2
}
暂时性死区,只要块级作用域下用let了声明变量,那么这个变量就和这个区域绑定,不再受外部影响。
二、作用域链(Scope Chain)
当我们定义一个函数并调用它时,函数
形成了一条作用域链。这条链由函数定义时所在的作用域和所有父级作用域组成,当查找一个变量时,JavaScript 引擎会从当前作用域开始搜索,如果找不到,则沿着作用域链向上查找
,直到找到或到达全局作用域为止。
var a = 1;
function fn1() {
var b = 2;
console.log(a); // 1:全局变量a
function fn2() {
var c = 3;
console.log(a); // 1:全局变量a
console.log(b); // 2:fn1内部的变量b
function fn3() {
console.log(a); // 1:全局变量a
console.log(b); // 2:fn1内部变量b
console.log(c); // 3:fn2内部的变量c
}
fn3();
}
fn2();
}
fn1();
解释: 当 fn3
被调用时,它首先访问自己作用域内的变量a
,b
,c
,但是都找不到就往上找,所以接着访问父级作用域 fn2
中的变量a
,b
,c
,找到了c
,但是a
,b
还是找不到,就接着往上找,访问作用域fn1
中的b
,再往上找,最后访问全局作用域中的变量 a
。这种逐级向上查找变量的过程就是作用域链。
三.词法作用域(Lexical Scope)
词法作用域是指函数访问变量的能力,词法作用域在哪就能访问这里的变量,而词法作用域取决于函数被定义
的位置,而不是函数被调用的位置,函数处于的作用域就是为该函数的词法作用域
。(很抽象,理解不了就看代码和图片)。
概括就是
:词法词法就是这个词写在了哪里
如图: 这就是作用域和词法作用域
再看下一个代码,应该打印出什么?
function bar() {
console.log(myName); // 打印Jerry
}
function foo() {
var myName = 'Tom';
bar();
}
var myName = 'Jerry';
foo();
如图:
解释: 最外面的全局作用域也就是 window
上的变量即 是bar
的词法作用域,从定义可知函数被定义在哪里,哪里就是它的词法作用域,当foo
被调用,bar
就会被调用,然后就会去bar
自己的作用域找,但是自己的作用域没有myName
,然后就去词法作用域里找myName
,所以bar
函数就可以访问到window
上的变量即myName = 'Jerry'
。
词法作用域的原理
词法作用域的关键在于函数在其定义时就决定了它可以访问哪些变量,这意味着,无论函数在哪里被调用,它始终能够访问其定义时所在的作用域中的变量。
总结出来就是
:看声明的位置,函数处于的作用域就是为该函数的词法作用域。
四、预编译(Hoisting)
预编译是指 JavaScript 在执行代码前先进行的一次变量声明和函数声明的提升操作。这意味着所有的变量声明和函数声明都会被提升到当前作用域的顶部(var
和fuction
会发生声明提升)。
var声明提升
console.log(a); // undefined
var a = 2;
js执行过程相当于
// var a;
//console.log(a); // undefined
//a = 2;
函数声明提升
foo(); // 输出 "foo"
function foo(){
console.log("foo");
}
let
vs var
vs const
- var 存在声明提升,let,const不存在
- let,const会和{} 形成块级作用域
- var 可以重复声明变量 let 不可以
- const用于声明一个常量,意味着值不能被修改
js预编译过程
- 发生在全局:
- 创建GO对象
- 找变量声明,将变量名作为GO的属性名,值为undefined
- 在全局找函数声明,将函数名作为GO的属性名,值为该函数体
- 发生在函数体内:
- 创建一个AO对象
- 找形参和变量声明,将形参和变量名作为AO的属性名,值为undefined
- 形参和实参统一
- 在函数体内找函数声明,将函数名作为AO的属性名,值为该函数体
不理解就看下面这个代码,下面推理过程
就是按照上面概念
来的:
function fn(a) {
console.log(a); // function
var a = 123
console.log(a); // 123
function a() {}
console.log(a); // 123
var b = function () {}
console.log(b); // function () {}
function c() {}
var c = a
console.log(c); // 123
}
fn(1)
过程:
全局预编译
// GO: {
// fn: function() {}
// }
函数fn预编译
// AO: {
// a: undefined => 1 =>function a() {}
// b: undefined => function b() {},
// c: undefined => function c() {}
// }
开始执行
//fn调用
//console.log(a); // function
//a = 123 即a: undefined => 1 =>function () {} => 123
//console.log(a); // 123
//console.log(a); // 123
//console.log(b); // function
// c=a 即c: undefined => function c() {} => 123
//console.log(c); // 123
所以使用var
时,有时会因为声明提升导致出现一些难以预料的结果,并且var允许重复声明同一个变量,这样会导致变量被覆盖和产生混乱,当全局声明时,还可能造成全局污染,所以ES6官方专门更新了两种新的声明变量的关键字,le
t和const
。
五、调用栈(Call Stack)
每当一个函数被调用时,调用栈就会添加一个新的栈帧(Stack Frame),该栈帧包含了函数执行所需的所有信息,如局部变量、参数以及返回地址等。当函数执行完毕并返回时,相应的栈帧就会从调用栈中移除。
var a = 2
function add(b, c) {
return b + c
}
function addAll(b, c) {
var d = 10
var result = add(b, c)
return a + result + d
}
addAll(3, 6)
如图:
首先创建全局GO
对象进栈,进到栈底,然后函数addAll
调用,接着创建AO
对象入栈,在addAll
函数里面有add函数
调用,又要创建AO
对象入栈,接着就是函数执行,add函数
执行完毕出栈,addAll函数
执行完毕出栈,依次执行完毕后出栈,这就是js的函数的调用过程。
还是上面那个词法环境
的例子:
function bar() {
console.log(myName); // 打印Jerry
}
function foo() {
var myName = 'Tom';
bar();
}
var myName = 'Jerry';
foo();
如图:
重点:
蓝色为全局GO
对象,黄色为foo的AO
对象,绿色为bar的AO
对象,所谓作用域链
,并不是在调用栈中从上到下
查找,而是看当前执行上下文变量环境中的 outer
指向来定,而 outer
指向的规则是,我的词法作用域
在哪里,outer
就指向哪里,而bar
声明在全局,所以的词法环境就是在全局,所以通过outer
指向的就是Jerry
。
六、闭包(Closure)
在javaScript中,根据词法作用域的规则,内部函数
一定能访问外部函数
中的变量,当内部函数被拿到外部函数之外调用时,即使外部函数执行完毕
,但是内部函数对外部函数中的变量依然存在引用
,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包。
- 闭包出现的原因:
js
中一个函数执行完毕就要出栈,但是内部函数又可以访问外部函数的变量,这就造成的矛盾,也就是闭包出现的原因。 - 闭包的作用:实现变量私有化
我们来看一个例子理解:
function foo() {
function bar() {
console.log(myName); // 打印Tom
}
var myName = 'Tom'
var age = 18
return bar
}
var myName = 'Jerry'
var fn = foo()
fn()
如图:
解释: 蓝色为全局环境,首先foo
的调用,黄色的foo
进栈,里面有bar
的声明,所以这时outer
指向foo
,并且它return
出去了一个函数bar
,foo
函数被执行完毕后要出栈,但是内部有一个函数还没调用,所以在旁边给它留下了一个小背包,里面有它所需要
的变量myName = 'Tom'
,而这个小背包就是我们所说的闭包,foo
执行完毕出栈画上×,然后就是fn
调用,也就是bar
的调用,导致绿色的bar
进栈,通过作用域链outer
指向找到小背包,所以输出Tom
七、this
关键字
this
是一个特殊的对象,它指向函数运行时的上下文,this
的值取决于函数是如何被调用的。
为什么要有this:
为了让对象中的函数有能力访问对象自己的属性
this可以显著的提升代码质量,减少上下文参数的传递
let obj ={
myName : 'Tom',
bar:function(){
//在对象内部的方法里面使用对象内部的属性
console.log(this.myName);
}
}
obj.bar();
this的绑定规则
- 默认绑定:当一个函数独立调用,即不带任何修饰符的时候,
this
就指向window
。(只要是默认绑定,this
就指向window
)。 - 隐式绑定:当函数的引用有上下文对象的时候(当函数被某个对象所拥有时),函数的
this
指向引用它的对象 - 隐式丢失:当一个函数被多个对象链式调用时,函数的
this
指向就近的那个对象 - 显式绑定:调用方法
call apply bind
强行掰弯this
指向我们想要的地方去 - new绑定:当使用构造函数创建对象时,构造函数内部的
this
会绑定到新创建的对象实例上
默认绑定
function foo(){
console.log(this); //window
}
foo();//函数被独立调用,指向全局
只要是默认绑定,this
就指向window
隐式绑定
var obj={
foo:function(){
console.log(this); // { foo: [Function: foo] }
}
}
obj.foo()//函数被引用,指向obj
foo函数
被obj
对象调用,函数的this
指向obj
隐式丢失
var obj = {
a: 1,
foo: foo //引用foo函数体
}
var obj2 = {
a: 2,
bar: obj
}
function foo() {
console.log(this.a); // 1
}
obj2.bar.foo()
正所谓将在外,军令有所不受,所以这里this
指向就近的那个对象obj
显式绑定
var obj ={
a:1
}
//此时this指向的是Window,但是call apply bind可以强行将this指向obj
function foo(x,y){
console.log(this.a);
}
//方法一::call 第一个参数,this需要指向的对象,后面接foo的参数
foo.call(obj,x,y);
//方法二::apply 第一个参数,this需要指向的对象,后面用数组来接foo的参数
foo.apply(obj,[x,y]);
//方法三:bind 第一个参数,this需要指向的对象,后面可以传foo的参数,也可以在返回函数中传参(会返回一个函数)
var bar = foo.bind(obj,x,y);
bar();
或
var bar = foo.bind(obj);
bar(x,y);
或
var bar = foo.bind(obj,x);
bar(y);
new绑定
function Person(name) {
this.name = name;
this.greet = function() {
console.log("Hello, my name is " + this.name);
};
}
var p1 = new Person("剑哥");
p1.greet(); // 输出: "Hello, my name is 剑哥"
用Personnew
了一个实例对象p1
,而构造函数内部的this
会绑定到新创建的对象实例上,所以这里的this
就是指向p1
。
注意:箭头函数没有this
机制
function foo(){
var bar = ()=>{
console.log(this); //window
}
bar();
}
foo();
写在箭头函数里的this
是其外层非箭头函数的this
,所以这里的this
其实是foo
里面的this
,也就指向window
。
总结
理解 JavaScript 中的作用域、闭包及 this
的行为是成为高级开发者的重要步骤。正确地使用这些特性可以极大地增强代码的功能性和可维护性。希望本文能够为你的学习之旅提供一些有价值的见解,并帮助你在 JavaScript 的道路上更进一步,感谢你的阅读,制作不易,有疏漏地方请指出。
转载自:https://juejin.cn/post/7414302239022284809