likes
comments
collection
share

你不知道的JavaScript(核心知识点概念详细整理)前言 JavaScript 是一门灵活且功能强大的编程语言,它不

作者站长头像
站长
· 阅读数 18

前言

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引入了letconst关键字,块级作用域是通过使用 {} 来定义的一个区域,在这个区域内部声明的变量(使用 letconst 关键字)只在这个区域内有效
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();

你不知道的JavaScript(核心知识点概念详细整理)前言 JavaScript 是一门灵活且功能强大的编程语言,它不

解释:fn3 被调用时,它首先访问自己作用域内的变量a,b,c,但是都找不到就往上找,所以接着访问父级作用域 fn2 中的变量a,b,c,找到了c,但是a,b还是找不到,就接着往上找,访问作用域fn1中的b,再往上找,最后访问全局作用域中的变量 a。这种逐级向上查找变量的过程就是作用域链。

三.词法作用域(Lexical Scope)

词法作用域是指函数访问变量的能力,词法作用域在哪就能访问这里的变量,而词法作用域取决于函数被定义的位置,而不是函数被调用的位置,函数处于的作用域就是为该函数的词法作用域。(很抽象,理解不了就看代码和图片)。

概括就是:词法词法就是这个词写在了哪里

如图: 这就是作用域和词法作用域

你不知道的JavaScript(核心知识点概念详细整理)前言 JavaScript 是一门灵活且功能强大的编程语言,它不 再看下一个代码,应该打印出什么?

function bar() {
  console.log(myName);  // 打印Jerry
}
function foo() {
  var myName = 'Tom';
  bar();
}
var myName = 'Jerry';
foo();

如图:

你不知道的JavaScript(核心知识点概念详细整理)前言 JavaScript 是一门灵活且功能强大的编程语言,它不

解释: 最外面的全局作用域也就是 window上的变量即 是bar的词法作用域,从定义可知函数被定义在哪里,哪里就是它的词法作用域,当foo被调用,bar就会被调用,然后就会去bar自己的作用域找,但是自己的作用域没有myName,然后就去词法作用域里找myName,所以bar函数就可以访问到window上的变量即myName = 'Jerry'

词法作用域的原理

词法作用域的关键在于函数在其定义时就决定了它可以访问哪些变量,这意味着,无论函数在哪里被调用,它始终能够访问其定义时所在的作用域中的变量。

总结出来就是:看声明的位置,函数处于的作用域就是为该函数的词法作用域。

四、预编译(Hoisting)

预编译是指 JavaScript 在执行代码前先进行的一次变量声明和函数声明的提升操作。这意味着所有的变量声明和函数声明都会被提升到当前作用域的顶部(varfuction会发生声明提升)。

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

  1. var 存在声明提升,let,const不存在
  2. let,const会和{} 形成块级作用域
  3. var 可以重复声明变量 let 不可以
  4. const用于声明一个常量,意味着值不能被修改

js预编译过程

  • 发生在全局:
  1. 创建GO对象
  2. 找变量声明,将变量名作为GO的属性名,值为undefined
  3. 在全局找函数声明,将函数名作为GO的属性名,值为该函数体
  • 发生在函数体内:
  1. 创建一个AO对象
  2. 找形参和变量声明,将形参和变量名作为AO的属性名,值为undefined
  3. 形参和实参统一
  4. 在函数体内找函数声明,将函数名作为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官方专门更新了两种新的声明变量的关键字,let和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)

如图:

你不知道的JavaScript(核心知识点概念详细整理)前言 JavaScript 是一门灵活且功能强大的编程语言,它不

首先创建全局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();

如图:

你不知道的JavaScript(核心知识点概念详细整理)前言 JavaScript 是一门灵活且功能强大的编程语言,它不

重点

蓝色为全局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()

如图:

你不知道的JavaScript(核心知识点概念详细整理)前言 JavaScript 是一门灵活且功能强大的编程语言,它不

解释: 蓝色为全局环境,首先foo的调用,黄色的foo进栈,里面有bar的声明,所以这时outer指向foo,并且它return出去了一个函数barfoo函数被执行完毕后要出栈,但是内部有一个函数还没调用,所以在旁边给它留下了一个小背包,里面有它所需要的变量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的绑定规则

  1. 默认绑定:当一个函数独立调用,即不带任何修饰符的时候,this就指向window。(只要是默认绑定,this就指向window)。
  2. 隐式绑定:当函数的引用有上下文对象的时候(当函数被某个对象所拥有时),函数的this指向引用它的对象
  3. 隐式丢失:当一个函数被多个对象链式调用时,函数的this指向就近的那个对象
  4. 显式绑定:调用方法call apply bind强行掰弯this指向我们想要的地方去
  5. 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 的道路上更进一步,感谢你的阅读,制作不易,有疏漏地方请指出。

你不知道的JavaScript(核心知识点概念详细整理)前言 JavaScript 是一门灵活且功能强大的编程语言,它不

转载自:https://juejin.cn/post/7414302239022284809
评论
请登录