JS基础——基于V8底层渲染机制变量提升机制、闭包、作用域、垃圾回收机制、函数执行过程、手撕call方法、bind预处理....

时间:2024-10-02 12:03:37

文章目录

    • 变量提升
      • 数据类型核心操作原理
        • 基本数据类型(值类型)
        • 可执行(调用)对象 funtion(){}
        • 非标准特殊对象
      • 《函数、作用域、上下文及浏览器中的垃圾回收机制》
          • 函数基本概念
          • 作用域
          • 闭包
          • 执行上下文
          • 普通函数执行的过程
          • 匿名函数具名化
          • 浏览器垃圾回收机制(内存管理机制)
          • js运行环境
          • this与call apply bind
            • this(函数执行的主体)
          • call
            • CALL中的细节
            • 阿里call面试题
            • 另一种思路
            • 手撕call方法
            • bind预处理this,不执行函数

变量提升

数据类型核心操作原理

基本数据类型(值类型)
  • number:数字
  • string:字符串
  • boolean:布尔类型
  • null:空对象指针
  • undefined:未定义类型
  • bigint:大数类型
  • symbol:唯一值类型引用数据类型 是操作地址的
  • 标准普通对象{}
  • 标准特殊对象
  • []数组
  • /^ $/正则 RegExp
  • Math
  • Date
  • ……
可执行(调用)对象 funtion(){}
非标准特殊对象

js代码运行在浏览器中,是因为浏览器给我们提供了一个供js代码执行的环境=>全局作用域(window/global
栈内存:作用域
1.提供一个供代码自上而下执行的环境
2.存储基本数据类型,值存储=>当栈内存被销毁,存储的基本值也被销毁了
堆内存:引用值对应的空间
1.存储引用数据类型的=>当堆内存被销毁,引用值彻底消失
堆内存释放:当堆内存没有任何变量或者其他东西占用,浏览器会在空闲的时候,自主的进行内存回收,把所有不被占用的堆内存销毁掉(谷歌浏览器) xxx=null
变量提升
当栈内存(作用域)形成,js代码执行之前,浏览器会把所有带有var/function关键词的进行提前‘声明’或者‘定义’,这种预先处理机制称之为‘变量提升’。
声明(declare):var a(默认值undefined)
定义(defined):a=12(定义其实就是赋值操作)
【变量提升阶段】
• 带var的值只声明不定义;带function的既声明又定义。
• 只提升等号左边的变量。
• 不管条件成不成立,都要进行变量提升。
• 新版标准浏览器,在{ }块级作用域中,对于function只声明不定义
var与let的区别
ES6中又新添两个关键字:let、const。
let、const: 不存在变量提升,且只能声明一次,不能重复声明 。
var 会给window增加一个属性,let 和const 不会。

《函数、作用域、上下文及浏览器中的垃圾回收机制》

函数基本概念

指一段在一起的、可以实现某个功能的程序,也叫做子程序、(OOP面向对象中)方法
function fn() {};// 创建函数
fn();//执行函数,可多次执行
函数的作用域由函数创建的位置决定;

作用域

作用域就是开辟一个栈内存
• 全局作用域:一打开页面,浏览器就会形成一个全局作用域
o 在全局作用域声明的变量是全局变量
• 私有作用域:函数创建时会形成一个私有作用域
o 在私有作用域声明的变量是私有变量,形参也属于私有变量
• 块级作用域:大括号{ }包含的部分会形成一个块级作用域,只对let和const声明的变量起作用
在私有作用域能拿到全局变量;在全局作用域拿不到私有变量
私有作用域形成后,先形参赋值,再进行变量提升,最后执行代码
变量提升只发生在当前作用域(例如:开始加载页面时只对全局作用域下的进行提升,因为此时函数中存储的都是字符串)
浏览器很懒,做过的事情不会重复执行第二遍,即,当代码执行遇到创建函数这部分代码后,直接跳过即可(因为在变量提升阶段已经执行过函数赋值过程了)
作用域链
它是变量的一种查找机制:假如一个私有作用的中的某个变量,不是私有变量;那么他就需要向上级作用域查找对应的变量,若没有该变量,则再向上级查找;一直找到全局;若全局仍没有;则会报错
上级作用域:函数在哪个作用域定义(创建)的,那么函数执行时形成的作用域的上级作用域就是谁

暂时性死区
• let 和const没有变量提升
• 但是在代码执行之前,也会先浏览一遍代码,把let和const的变量私有化
• 在let和const声明变量的一行之前是不能调用的
let a=12;
function fn(){
console.log(a);//报错
let a=10;
}
function fn(){
(a);//undefined
var a=10;
}
arguments与形参之间的映射关系
function fn(a,b) {
(arguments);//2 3
arguments[0]=100;
(a, b);// 100 3
b=300;
(a, b);//100 300
}
fn(2,3)
//若映射关系一开始没建立起来,后面就没法建立了
function fn(a,b) {
(arguments);
arguments[0]=100;
(a, b);
b=300;
(arguments[1]);//undefined
}
fn(2);

闭包

• 一个不销毁的作用域
• 变量的一种保护机制,保护私有变量不受外界影响
• 函数执行返回值是一个引用数据类型,则该作用域不会被销毁
function fn() {
var a=12;
var b=13;
return {
a:12,
b:13,
}
}
var b=fn();//不销毁
fn();//立即销毁
外界普遍认为闭包:函数执行产生一个不被释放的私有的上下文,这样不仅保护里面的私有变量不受污染,而且还可以把这些信息存储下来(保护+保存 ),供其下级上下文使用。 闭包是一种机制,不要局限于具体的代码。

执行上下文

• 浏览器
• webview WebApp(Hybride混合APP)

浏览器能够运行js代码,是因为浏览器提供了代码运行的环境:栈内存(Stack)
• 栈内存也是从计算机的内存分配出来的一块内存
• 执行环境栈 ECStack(Execution Context Stack)
执行代码的过程中,为了区分是在哪个环境下执行(全局、函数、块…),首先会产生一个执行上下文
• EC(G)全局上下文,全局代码在这执行;只有在页面关闭的时候,才能释放
• EC(X)某个函数的执行上下文

普通函数执行的过程
  1. 形成一个全新的私有上下文 Ec(?) ->进栈执行 AO(Active Object)变量对象
  2. 代码执行前处理的事情 + 初始化作用域链 + 声明THIS指向 + 初始 arguments实参集合,类数组集合 + 形参赋值 + 变量提升
  3. 代码执行
  4. 出栈OR不出栈
匿名函数具名化

一般自执行函数都是匿名函数;但是我们可以给其设置为实名函数(具名函数),只不过及时设置了函数名,和平时普通的具名函数还是有区别的

  1. 匿名函数具名化之后,设置的函数名,只能在函数内部的上下文中使用(和外界上下文中的变量没有关系),相当于当前私有上下文中的一个私有变量
  2. 这个设置的函数名,不仅只能在私有上下文中用,而且还不能修改其值
  3. 我们可以基于重新声明这个变量的方法,修改它的值(相当于我们自己声明的优先级会很高,告诉浏览器,以我们自己声明的为主)
(function func() {
           (func); //=>当前函数
})();
(func); //=>Uncaught ReferenceError: func is not defined
"use strict";
       (function func() {
           (func); //=>函数
           func = 100; //=>修改这个操作不会起到作用,而且在严格模式下会报错 Uncaught TypeError: Assignment to constant variable.
           (func); //=>函数
       })();
(function func() {
           // 变量提升阶段:var func;
           (func); //=>undefined
           var func = 100;
           (func); //=>100
       })();
(function func() {
       // func设置的函数名字,类似于私有变量,但是本质上不是私有变量
       let func = 100;
       (func); //=>100
       /*function func() {
           ('OK');
       }*/
})();
/* var b = 10;
   (function b() { //=>具名化的名字和全局上下文中的变量没有关系
       b = 20;
       (b); //=>匿名函数(修改值没用)
   })();
   (b); //=>10 */
/* var b = 10;
(function b() {
   var b = 20;
   (b); //=>20
})();
(b); //=>10 */
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
浏览器垃圾回收机制(内存管理机制)

• 引用标记(谷歌)
• 引用计数(IE低版本)
引用标记
当内存被占用时,浏览器会标记该内存被占用true;否则,被标记为false。当浏览器空闲时,会回收所有未被标记的内存。
计数标记
当内存被引用时,浏览器会标记有几处引用(1,2,3,…);否则为0。当浏览器空闲时,会回收所有为0的内存。

• 总结:只要内存被占用,内存就不能被释放;如果没有被占用,浏览器空闲时就会释放。
Heap堆内存释放
let obj={XXX. XXX}; obj=null; 空对象指针
Stack栈内存释放
+全局上下文:加载页面形成、关闭页面释放
+私有上下文:
+默认函数执行完,形成的上下文都会被出栈释放
+特殊:如果当前上下文中的某些内容(一般指一个堆),被上下文以外的内容所占用了,则不能出栈释放!(就形成了闭包)

js运行环境

• 浏览器
• webview WebApp(Hybride混合APP)

浏览器能够运行js代码,是因为浏览器提供了代码运行的环境:栈内存(Stack)
• 栈内存也是从计算机的内存分配出来的一块内存
• 我们叫做:执行环境栈 ECStack(Execution Context Stack)
执行代码的过程中,为了区分是在哪个环境下执行(全局、函数、块…),首先会产生一个执行上下文
• EC(G)全局上下文,全局代码在这执行;只有在页面关闭的时候,才能释放
• EC(X)某个函数的执行上下文
• ES6新增{}块级作用域

this与call apply bind
this(函数执行的主体)

• 全局上下文的this是window
• 块级上下文没有自己的this,所用的this是继承上下文的this(箭头函数也是)
事件绑定
事件绑定的this,指定绑定的元素
普通函数执行
其他情况看点,点前面是谁,this就是谁;没点就是window

call、apply、bind强制改变this指向问题
• 都是用来改变某一个函数中this关键字指向的

call

1、[fn].call([this],[param]…)
• :当前实例(函数fn)通过原型链的查找机制,找到function。prototype上的call方法
• () : 把找到的call方法执行
• 当call方法执行的时候,内部处理了一些事情
• =>首先把要操作函数中的this关键字变为call方法第一个传递的实参值
• =>把call方法第二个及第二个以后的实参获取到
• =>把要操作的函数执行,并且把第二个以后的传递进来的实参传给函数
let sum=function(a,b){
(this,a+b);//=>opt
};
let opt={n:20};
(opt,20,30)//=>call执行 call中的this是sum 把this(call中的)中的“this关键字”改为opt 把this(call中的)执行,把20,30分别传递给它 //=>sum中this:opt a=20 b=30
(opt)
// 找到上的call方法(也是一个函数,也是函数类的一个实例,也可以继续调用call/apply等方法) =>A(函数)
//(opt) 继续找到原型上的call方法,把call方法执行:把A中的this关键字修改为opt,然后把A执行

CALL中的细节

• 1.非严格模式下,如果参数不传,或者第一个传递的是null/undefined,THIS都指向WINDOW
• 2.在严格模式下(加 ‘use strict’ 字符串就是严格模式),第一个参数是谁,THIS就指向谁(包括null/undefined),不传THIS就是undefined
(obj, 10, 20);//=>this:obj a=10 b=20
(10, 20);//=>this:10 a=20 b=undefined
();//=>this:window a=undefined b=undefined
(null);//=>this:window
(undefined);//=>this:window

阿里call面试题

/*
= function (context, …arg) {
//=>my_call方法中的this就是我们要处理的那个函数(fn/sum…)
().replace(‘this’, context);
this(…arg);
};
*/
function fn1() {
(1);
}
function fn2() {
(2);
}
(fn2);//=>1

/*
执行CALL方法

  • 方法中的THIS:FN1
  • 方法中的CONTEXT:FN2
    */
    (fn2);//=>2
/*
* 执行的是最后一个CALL方法
*   :
*   :FN2
*/
//=>第一次执行最后面的CALL,代码执行第一步
//().replace('this', FN2);
// =function (context, ...arg) {
//     ().replace('this', context);
//     FN2(...arg);
// };
//=>第一次执行最后面的CALL,代码执行第二步
//();
/*
*  第二次执行CALL方法
*    CONTEXT='UNDEFINED'
*    ().replace('this', undefined);
*    FN2();
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
另一种思路

(fn2);
/*

  • CALL中的THIS是FN1,把FN1中的THIS关键字修改为FN2,然后再把FN1执行 =>“CALL方法中的THIS是谁,最后执行的就是谁”
    /
    (fn2);
    /
  • 第一次执行最末尾的CALL,CALL中的THIS是,先把中的THIS改为FN2,然后让执行
  • 第二次CALL执行,方法中的THIS已经被上一次修改为FN2了,所以参考“THIS是谁就执行谁”的标准,执行的是FN2
    */
    (1)//报错
手撕call方法
 = function() {
   let [thisArg, ...args] = [...arguments];
   if (!thisArg) {
       //context为null或者试undefined
       thisArg = typeof window === 'undefined' ? global : window;
   }
   //this指向试当前函数func
    = this;
   //执行函数
   let result = (...args);
   delete ;
   return result;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
bind预处理this,不执行函数

bind:语法和call一模一样,唯一的区别在于立即执行还是等待执行
• (obj,10,20) 改变FN中的THIS,并且把FN立即执行,返回的是fn函数执行的结果
• (obj,10,20) 改变FN中的THIS,此时的FN并没有执行(不兼容IE6~8),把改变this后的函数体返回
let fn = function (a, b) {
(this);
};
let obj = {name: “OBJ”};
= fn;//=>把FN绑定给点击事件,点击的时候执行FN
= fn();//=>在绑定的时候,先把FN执行,把执行的返回值(UNDEFINED)绑定给事件,当点击的时候执行的是undefined
//=>需求:点击的时候执行FN,让FN中的THIS是OBJ
= fn;//=>this:document
= (obj);//=>虽然this确实改为obj了,但是绑定的时候就把fn执行了(call是立即执行函数),点击的时候执行的是fn的返回值undefined
= (obj);//=>bind属于把fn中的this预处理为obj,此时fn没有执行,当点击的时候才会把fn执行
function fe(){
(this)
}
(1)//=> Number(1) 实例化后的1
apply (传参方式不同)
apply:和call基本上一模一样,唯一区别在于传参方式
• (obj,10,20)
• (obj,[10,20]) APPLY把需要传递给FN的参数放到一个数组(或者类数组)中传递进去,虽然写的是一个数组,但是也相当于给FN一个个的传递