高级js系列3 函数运行机制 this指向

Posted lin-fighting

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高级js系列3 函数运行机制 this指向相关的知识,希望对你有一定的参考价值。

其他类型转为字符串

1 val.toString

2 String(val)

区别:

  • String转为原始类型的时候,基于引号包起来,binInt回去掉n
    转化对象的时候,

    其解析顺序就是调用对象的Symbol.primitive、valueOf、
    如果valueOf返回的是原始值,就将其转化为string,如果不是原始值,则调用toString方法。

  • toString的每个类型的原始链上的一个方法,用于转化为string类型。

3 隐式转换 +号

规则:
1 两边有一边是字符串的时候,则会变为字符串拼接。
2 一边是对象的时候,按照Symbol.Primitive、valueOf、toString处理,变为字符串后,再按照字符串拼接。


new Number(10)的valueOf是number类型10,直接相加即可。
3 特殊情况
对象出现在左边
{} + 10 => 10

他会认为{}是代码块,处理的只是+10的操作,这里是两步操作
还有

也会认为是代码块。+10会被认为是转化为数字操作
看一个例子

100 + true + 22.2 + null + +undefined + "Tencent" + [] + null + 9 + false

首先100+true,既没有string也没有对象,所以直接将true转为number
101 + 22.2 =》 122.2+null,null转为0 + undefined,undefined转为number是NaN,所以就变成了NaN,+Tencent =》 NaNTencent
再加【】,[]调用toString是‘’,再加null,此时右边是字符串,将null->'null’就变成了NaNTencentnull,后面的9跟false都会转为字符串,结果就是

解决浮点数计算丢失精度问题。


有的有问题,有的没问题。因为所有的值在计算机底层存储的时候是以二进制存储的,js通过number类型表示整数和浮点数,通过64位的二进制存储。
第0位,符号位,0表示正 1 表示负
1-11,存储指数部分
12-63 存储小数部分
浏览器会认为我们写的数字是10进制的,所以浏览器有一个默认操作,将我们写的10进制转为2进制再交给计算机。

  • 十进制转二进制的计算 n.toString(2)
    先看整数部分 12(10进制)=》2进制
    12/2 = 6…0
    6/2=3…0
    3/2=1…1
    1/2=0 …1
    所以12的二进制就是1100,从后往前串在一起。
  // 实现转换二进制
        const decimal2bianry = (decimal) => {
            let binary = []
            let interge = Math.floor(decimal / 2) //向上取整
            let remainder = decimal % 2
            binary.unshift(remainder)
            while (interge) {
                remainder = interge % 2
                binary.unshift(remainder)
                interge = Math.floor(interge / 2)
            }

            return binary.join('')
        }

自己也可以实现(对整数实用),使用toString(‘2’)也可以。
而浮点数在计算机存储的时候,存储的二进制最大位只能是64位,可能被割舍一部分,导致本身和原来的十进制已经不一样了,所以0.1+0.2才会不等于0.3。计算机底层会将0.1与0.2的二进制相加,最后转为10进制给浏览器,这可能是一个很长的值,但是浏览器也可能存在长度的限制,会截掉一部分,最后面的全是零的省略掉。
解决方法:乘以一定的系数变成整数运算再除以系数。

//获取系数
        const getCoefficient = (num) => {
            num = num + ''
            let [, char = ""] = num.split('.')
            return Math.pow(10, char)
        }

        const plus = (num1, num2) => {
            num1 = +num1
            num2 = +num2
            if(isNaN(num1) || isNaN(num2)){
                return ;
            }
            const coefficient = Math.max(getCoefficient(num1), getCoefficient(num2))
            return (num1 * coefficient + num2 * coefficient) / coefficient

        }


函数的运行机制

先复习一下,浏览器打开页面的时候开辟两个内存,一个栈,一个堆。

  • 在堆内存会开辟一小块空间,叫做GO全局对象,用来存放全局的api如setTimeout这些,还有存放var/function生命的变量。
  • 栈内存用来执行代码,以及存放一些原始值,还有基于let/const等声明的变量。函数在执行的时候,每个上下文都会创建一个变量对象叫做VO,全局变量对象里存放着window,指向GO。还有基于let/cinst/class等声明的变量。
  • 创建一个函数的时候,也就是function A(){}的时候,会进行变量提升。然后在GO创建一个A变量。然后在堆内存中开辟一块空间,比如地址是0x001。
  • 接着存储一些内容到空间中(创建函数做的事情)
  1. 作用域([Ecope]: EC(G)):在哪个上下文中创建的,作用域就是谁(比如在全局作用域创建的,作用域就是全局作用域);
  2. 函数体代码当作字符串存储起来;
  3. 函数也是对象,也会存储一些建值对。(如 函数名,length: 参数个数)
  • 最后再把堆内存地址0x001给到GO中的A变量。
    *

接着就是函数运行,x和fn已经提前声明过了,所以到var x = [12,23]的时候会继续开辟一块空间放数组的内容地址为0x002,GO的x关联的值为0x002,然后到function fn(){}这段代码,因为我们已经变量提升过了,所以整断跳过。
到fn(x)

  • 函数执行:所发生的步骤
    **1 产生一个全新的私有的上下文,**而且我们知道每个上下文都有一个变量对象,叫AO(也叫变量对象),用来存储当前上下文中声明的变量。AO是VO的分支。
    2 初始化作用域链 SCOPE=CHAIN <自己的上下文,函数的作用域(上级上下文)> 上图就是自己的上下文+全局上下文 <EC(FN) , EC(G)>
    3 初始化this,上图this指向window3. 初始化this,
    4 初始化arguments,类数组对象 {0: 0x001, length: 1}
    5 形参赋值,形参也是私有变量,存储在当前上下文的AO中。 y = 0x002
    6 变量提升,私有上下文中声明的是私有变量,存储在AO中。
    7 执行代码
    8 执行代码过程中,遇到一个变量,会先判断端是否是自己的AO上的变量,如果是,就是私有的,所有操作与外界无关。如果不是自己AO上的,就不是私有的变量,则会向其上级上下文中去查找找到改变量。。。。直到找到EC(G)全局上下文为止,如果没找到则报错。xx is not defined。如果是设置变量值,则相当于给GO设置对应属性,比如(x = 2)这种操作。就会在GO上加一个x变量,值为2.
  1. 形参是私有变量,但是其特殊性就是当传对象的时候,和外部传入的对象公用一个堆地址。

10 一般情况下,函数执行完毕会进行出栈操作,优化性能。
例子:

  • 执行A(1)的时候会输出1,这个正常。关键是在函数A里面,他直接重写了A,因为在函数里面,它会从当前的上下文去找A,找到VO(G)的时候就发现了A,然后对A进行重写。在执行A(2)的时候,此时A的作用域链应该是EC(匿名函数)-EC(重写前的A)-EC(G),所以执行a+b++的时候,a会从外部作用域找到,也就是外层的刑参a,这个a存放在EC(A)的AO中,刚刚才被赋值为1,而后有进行了a++操作,所以是2,2+2++就是4,会输出4。

垃圾回收机制

标记清除法(chrome)

在开辟内存的时候,还有会一个标记,记录着这块内存是否被引用。若没有被引用的就会被浏览器在空闲的时候进行垃圾回收,

引用计数法

引用计数不是按值引用,比如当前有一个堆内存,如果被一个变量暂用,那么就引用次数就记为1,如果还有别的变量暂用,就记为2,当没有关联的时候,就减1.当为0的时候,就会被垃圾回收机制回收。缺点:两个变量互相引用的时候,永远为1。

对于栈内存,比如全局上下文,函数私有上下文,块级私有上下文。什么时候被回收呢?

  • 全局上下文在打开页面执行代码的时候就会生成, 只有当页面关闭的时候才会释放。
  • 私有上下文:如果当前私有上下文的某个关联的内容(一般指堆地址,比如返回一个函数),被当前上下文以外的事物占用了,那么这个上下文不能被出栈释放(如闭包),对应函数的堆内存不会被回收,对应的私有上下文(栈),在执行完毕后不会出栈处理。这样私有上下文中的私有变量也被保存起来,否则,一般函数中的代码执行完毕,私有上下文就会被出栈释放。
  • 闭包:他是一种机制,函数执行会产生一个私有上下文(作用域),可以保护里面的私有变量不受外界干扰,防止全局变量污染,我们把函数执行的这种机制称为闭包。
  • 另一种说法:函数执行的时候,产生一个不被释放的私有上下文,除了私有变量被保护起来以外,还可以把他的值保存下来,供下级上下文使用,这样的保护+保存机制,就是闭包。
    上下文与作用域与是一个东西,都是执行产生的,区别可能是:上下文是函数执行的时候自己产生的环境,作用域则是函数创建时所在的环境。
  • 函数每次会执行都会产生一个全新的上下文,所有的事情基本上都会重新来一遍,跟前面执行的上下文没有一点关系
  • 例子

声明变量的方式区别

es5 var function
es6 let const class import

let & const

let const 声明的都是变量,都会存储到当前上下文的AO/VO中

  • 1let 声明的变量,后续可以根据需求改变“变量”和“值”之间的指针指向,而const声明的变量,不允许改变变量与值之间的指针关系;
let a = 1;
a = 2; //可以
const c = 2;
c = 4; //报错
//但是不改变指针关系,如果指针指向的是堆地址,即可以修改
const d  = {a: 1}
d.a = 4 //可以
  • 2 .const 声明的变量的同时必须赋值

let & var

  • 1 . var会存在变量提升,而let不会;(函数只有创建的时候会提升并且定义;而函数表达式并不会提升。)
  • 2 . 全局上下文下,基于var,function声明的变量直接存储在GO中的,而let const存储在VO(G)中
  • 3 . var允许多次重复声明(实际上浏览器只声明一次,只不过此法允许),但是let不允许(错误发生在此法解析阶段);
var a = 1;
var a = 2;  //可以
let d = 1;
let d = 2; //报错 d已经声明过了

原因: 浏览器从服务器获取到了js代码(字符串),首先进行词法分析,生成一个完整的AST树;然后浏览器底层(如c++)按照AST一步一步执行;

// AST
 AST = {
 	//全局上下文
 		EC(G): {
			VO(G): {
				d
			}
		}
 	}

此时全局上下文中已经有一个d了,如果再声明一个d就会报错。

  • 4 .在{}中(排除对象和函数,if(){}这些都可以),只要出现let /const /class /function /等关键词,都会产生一个“私有块级上下文”,var不会产生私有块级上下文,并且块级上下文对var没有影响;
  {
            let a = 2
             var c = 4

        }
        console.log(a); //报错,找不到a
        console.log(c); //如果可以正常运行,那么将打印4

如:

 var a = 1
        if (true) {
            // 作用域链: <EC(BLOCK), EC(G)>
            // 初始化this 没有这一步
            // 初始化arguments 没有这一步
            // 形参赋值 没有这一步
            // 变量提升 --
            //代码执行
            console.log(a);
            var a = 10
            let d = 10
            console.log(d); // 10
        }
        console.log(d);  // 报错  d没找到
        console.log(a); // 10  块级上下文对var无效
  • 5 . 暂时性死区
    使用未声明的变量,报错;
console.log(a) //报错
let a = 1

总结:

let & var区别:
1 变量提升
2 重复声明
3 GO与VO
4 块级作用域对var/let的区别
5 暂时性死区

几道面试题:

console.log(foo). // undefined
	{
	console.log(foo). // undefined
	function foo(){}
	foo = 1
	console.log(foo). // 1
	}
	console.log(foo) // foo(){}

如果是老版本的浏览器,没有块级作用域的概念,最后打印出来就是1;

  • 而新版本的浏览器,加入了块级作用域的概念。如上,当遇到大括号(除了函数/对象)中的function,在其外层上下文变量提升的时候会提前声明,但不会赋值!这是区别1;
  • 区别2:如果大括号中出现let /const /function,会产生一个私有块级上下文;私有上下文变量提升的时候,会声明foo并且赋值。
  • 区别3:然后继续执行代码,对于块级上下文,遇到function foo(){}这段代码的时候,也就是当“遇到创建函数”这一行代码的时候,会把私有上下文中,运行这段代码之前,所有对foo的操作(变量提升:声明+赋值)同步到全局中,也就是说全局的foo不再是unfined,而是foo(){}。但是运行这行代码之后的操作只跟私有上下文有关!当下次遇到“创建函数”这一行代码的时候,会继续将这行代码之前对foo的操作也同步到外部上下文去。
  var x = 1
      function foo (x, y=function(){x=2}){
          var x= 3   //块级上下文执行
          y()  // y是在函数上下文中形参赋值的时候创建的,所以其外层作用域指向函数
          	     // 上下文,也就是说x=2修改的是函数上下文的x,从5变为2
          console.log(x);  //块级上下文执行,x=3
      }
      foo(5)
      console.log(x); // 1不变

这道题涉及一个知识点!函数形参赋值的坑!
当函数形参接受的值为undefined的时候就会使用默认值;那么有个问题:

  • 有两个条件,第一:函数使用了形参赋值默认值!第二:在函数体中,基于let/var/const又声明了变量(function声明的不散),只要满足这两个条件,该函数在执行的时候,将会产生两个上下文,函数私有上下文和块级私有上下文!,并且块级私有上下文的上级上下文就是函数私有上下文。

  • 我们知道函数运行时会做一堆操作,作用域链形成,形参赋值,变量提升等等。函数私有上下文只会做作用域链形成,形参赋值这两个操作;接着就轮到块级私有上下文工作了;

  • 其中,如果块级上下文的某个变量和函数私有上下文的某个变量名字相同的时候,则在块级上下文代码执行之前,首先会把函数私有上下文中的变量值,同步给块级上下文中的这个变量。

THIS指向

一般来说:this指向函数执行的主体,就是谁执行指向谁。

1 事件绑定

2 函数执行(普通函数,成员访问,匿名函数,回调函数)

3 构造函数

4 箭头函数, 生成函数

5 基于call/apply/bind强制修改this指向

  • 全局上下文中的this指向window,严格模式下指向undefined。
    块级上下文中没有自己的this,所有的this都是继承上级上下文中的this(箭头函数的运作规则)

1 事件绑定

  • 1
    DOM0 事件绑定 如dom.onclick = function(){}
    DOM2
    xxx.addEventListener(‘click’, function(){}) //不兼容ie78
    xxx.attachEvent(‘onclick’, function(){})
    事件绑定就是给当前元素的某个事件绑定方法(此时是创建,还未执行),当事件行为触发的时候,浏览器会把绑定的方法执行,此时函数中的this指向当前元素对象本身。
    特殊:基于attachEvent实现的事件绑定,方法执行,方法中的this是指向window。

2 函数执行

  • 正常普通函数的执行一般看是谁调用他就指向谁?比如a.fn()指向a;
    单纯的写fn(),本质上是window.fn(),this指向window。但是在严格模式下,this指向了undefined。
  • 匿名函数的执行:
    函数表达式,自执行函数,回调函数,括号表达式;
    • 括号表达式:(a.fn)(),指向a;(10, a,fn)(),正常执行a.fn(),但是this会受到影响,指向了window/undefined。
    • 函数表达式等同于普通函数
    • 自执行函数,(fn(){console.log(this)})(); this的指向也是window/undefined
    • 回调函数,把一个函数A作为实惨,传递给另一个函数B,在B中可执行A。如:

      一般回调函数的this都是指向window/undefined。forEach的回调函数默认this一般也指向window/undefined,除非用第二个参数改变。
      setTimeout等的this也是指向window。
      例子:
  var x = 3,
       obj = {
           x: 5
       }
       obj.fn  = (function(){
           this.x *= ++x;
           return function(y){
               this.x *= (++x) + y;
               console.log(x);
           }
       })()
       var fn = obj.fn
       obj.fn(6)
       fn(4)
       console.log(obj.x, x);

首先x,obj会被创建,接着会执行fn的自执行函数。自执行函数this指向window,所以x会等于3,而变量x会指向外层的全局变量3,所以此时this.x = 3 * 4 = 12。返回一个函数赋给obj.fn。。。
结果为: 13 234 95 235;

以上是关于高级js系列3 函数运行机制 this指向的主要内容,如果未能解决你的问题,请参考以下文章

JS高级---函数中的this的指向,函数的不同调用方式

JS高级---apply和call都可以改变this的指向

JS高级——深入剖析函数中的this指向问题

js高级

JS基础系列-聊聊this

JS高级 构造函数和原型 + 继承 + ES5新增方法