JavaScript基础复习

423 阅读20分钟

JavaScript基础

基础是最重要的东西,部分学习整理自冴羽大佬的博客,地址github.com/mqyqingfeng…

JavaScript的数据类型

原始数据类型:

  • booleantrue/false

    关于boolean的数据类型转换:

    数据类型转换为true的值转换为false的值
    booleantruefalse
    string任何非空字符串“”(空字符串)
    number任何非零数字0和NaN
    object任何对象null
    undefinedN/A(not applicable 不适用)undefined
  • null:空对象指针

    注意:typeof null的结果为object

  • undefined:未定义的值(对变量进行了生命却没有进行赋值操作)

  • number:数值型,对于那些极大或者极小的数,可以用e来表示法(即科学计数法)表示的浮点数值表示,例如:3.125e7表示3.125乘十的7次方

    由于内存的限制,ECMAScript并不能保存世界上所有的数值,最小数值存在Number.MIN_VALUE中---在大多数浏览器中,这个值为5e-324;最大数值存在Number.MAX_VALUE中---在大多数浏览器中,这个值为1.7976931348623157e308,如果某次计算中超过了这个范围,将会自动转换成特殊的Infinity值(无穷大),如果是负数则为-Infinity

    • 整数

    • 浮点数(小数)

    • NaN:非数值,这是一个特殊的数值,用这个数值来表示一个本来要返回数值的操作数未返回数值的情况(避免抛出错误)

      NaN本身有两个非同寻常的特点:

      1.任何涉及NaN的操作都会返回NaN

      2.NaN与任何值都不相等,包括他本身

    Number()函数的转换规则:

    • 如果是booleantrue和false分别被转换成0和1
    • 如果是数字:只是简单的传入和返回
    • 如果是null:返回0
    • 如果是undefined:返回NaN
    • 如果是string,遵循以下规则:
      • 字符串中只包含数字(包括正号和负号),转换成10进制数,忽略前导零
      • 字符串中包含有效的浮点格式,转换成浮点数,忽略前导零
      • 字符串中包含有效的十六进制格式(0xf),转换成相同大小的十进制数
      • 空字符串转换成0
      • 字符串中包含上述以外的字符,转换成NaN
    • 如果是对象,则调用对象的valueof()方法,然后依照前面的转换规则返回的值,如果返回NaN,则调用对象的toString()方法,然后再次依照前面的转换规则进行返回
  • string:以单引号''或者双引号""括住的

    String()函数遵循以下转换规则:

    • 如果值有toString()方法,则调用该方法并返回结果
    • 如果值是null,则返回"null"
    • 如果值是undefined,则返回"undefined"
  • symbol:符号,是ES6中新增的数据类型,symbol表示独一无二的值,通过Symbol函数调用生成,由于生成的symbol值为原始类型,所以Symbol函数不能使用

引用数据类型

  • object:对象,即一组数据和功能的集合

    每个object的实例都具有以下几个属性和方法:

    • constructor():保存着用于创建当前对象的函数
    • hasOwnProperty(propertyName):用于检查给定属性是否在当前对象实例中(而不是在实例的原型中)
    • isPrototypeOf(object):用于检查传入的对象是否是当前对象的原型
    • propertyIsEnumerable(propertyName):用于检查给定的属性是否能够使用for...in...语句来枚举
    • toLocaleString():返回对象的字符串表示,该字符串与执行环境相对应
    • toString():返回对象的字符串表示
    • valueOf():返回对象的字符串、数值、布尔值表示

判断JavaScript的数据类型

typeof

通过typeof操作符来判断一个值属于那种数据类型

typeof 'seymoe'    // 'string'
typeof true        // 'boolean'
typeof 10          // 'number'
typeof Symbol()    // 'symbol'
typeof null        // 'object' 无法判定是否为 null
typeof undefined   // 'undefined'

typeof {}           // 'object'
typeof []           // 'object'
typeof(() => {})    // 'function'

从上面的代码可以看出:

  • null的判断有误差,得到的结果是object
  • 操作符对对象类型及其子类型,例如函数(可调用对象)、数组(有序索引对象)等进行判断,则除了函数都会得到一样的结果object

instanceof

通过instanceof操作符也可以对对象类型进行判断,其原理是测试构造函数的prototype是否出现在被检测对象的原型链上

[] instanceof Array            // true
({}) instanceof Object         // true
(()=>{}) instanceof Function   // true

但是instanceof也不是万能的,举个🌰:

let arr = []
let obj = {}
arr instanceof Array    // true
arr instanceof Object   // true
obj instanceof Object   // true

arr 数组相当于 new Array() 出的一个实例,所以 arr.__proto__ === Array.prototype,又因为 Array属于 Object 子类型,即Array.prototype.__proto__ === Object.prototype,因此 Object 构造函数在 arr 的原型链上。所以 instanceof 仍然无法优雅的判断一个值到底属于数组还是普通对象。

Object.prototype.toString()

通过Object.prototype.toString()方法对数据类型进行判断

Object.prototype.toString.call({})              // '[object Object]'
Object.prototype.toString.call([])              // '[object Array]'
Object.prototype.toString.call(() => {})        // '[object Function]'
Object.prototype.toString.call('seymoe')        // '[object String]'
Object.prototype.toString.call(1)               // '[object Number]'
Object.prototype.toString.call(true)            // '[object Boolean]'
Object.prototype.toString.call(Symbol())        // '[object Symbol]'
Object.prototype.toString.call(null)            // '[object Null]'
Object.prototype.toString.call(undefined)       // '[object Undefined]'

Object.prototype.toString.call(new Date())      // '[object Date]'
Object.prototype.toString.call(Math)            // '[object Math]'
Object.prototype.toString.call(new Set())       // '[object Set]'
Object.prototype.toString.call(new WeakSet())   // '[object WeakSet]'
Object.prototype.toString.call(new Map())       // '[object Map]'
Object.prototype.toString.call(new WeakMap())   // '[object WeakMap]'

我们可以发现该方法传入任何类型的值都能正确返回对应的对象类型,但是需要注意几点:

  • 该方法本质就是依托Object.prototype.toString()方法得到对象内部属性 [[Class]]
  • 传入原始类型却能够判定出结果是因为对值进行了包装
  • nullundefined 能够输出结果是内部实现有做处理

面试遇见的几个问题

null是对象吗?为什么?

结论:null不是对象

解释:虽然typeof null会输出object,但这只是JavaScript存在的一个历史悠久的Bug。在最初版本的JavaScript中使用的是32位系统,为了性能考虑使用地位存储变量的类型信息,000开头表示是对象而null表示全为0,所以错误地判断为object

"1".toString()为什么可以调用?

解释:其实在这个语句执行的过程中做了这样的几件事情:

var s = new String();   // 创建String实例
s.toString();   // 调用实例方法
s = null;    // 执行完毕之后立即销毁实例

这一整个过程体现了基本包装类型的性质,而基本包装类型恰恰属于基本数据类型,包括booleannumberstring

0.1+0.2为什么不等于0.3?

解释:0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数将会被截取掉,此时就已经出现了精度偏差,相加后因为浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004

看一下代码,然后请问输出了什么?为什么?

var a = {};
var b = {key:"a"};
var c = {key:"b"};
a[b] = "123";
a[c] = "456";
console.log(a[b]); // 输出456
// 在JavaScript中的对象键名只能是字符串,用b/c这种对象做键名时会先调用toString方法,b/c调用toString方法得到的都是[object Object],所以更新更新值时等于更新了同一个键的值

手动实现一个instanceof的功能:

核心:基于原型链的向上查找

function myInstanceof(left, right) {
    //基本数据类型直接返回false
    if(typeof left !== 'object' || left === null) return false;
    //getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
    let proto = Object.getPrototypeOf(left);
    while(true) {
        //查找到尽头,还没找到
        if(proto == null) return false;
        //找到相同的原型对象
        if(proto == right.prototype) return true;
        proto = Object.getPrototypeof(proto);
    }
}

JavaScript中的with语句

with语句的作用是将代码的作用域设置到一个特定的对象中,语法如下:

with (特定对象) statement;

严格模式下不允许使用with语句,否则将会视为语法错误

定义with语句的目的主要是为了简化多次编写同一个对象的工作,例如:

var ss = location.search.substring(1);
var hostName = location.hostname;
var url = location.href;
// 可以使用 with 改写为
with(location){
  var ss = search.substring(1);
  var hostName = hostname;
  var url = href;
}

JavaScript的执行上下文栈

JavaScript代码的执行顺序

如果要问JavaScript的执行顺序,很多人都会有很直观的印象(从上到下,从左到右)

我们看下面的代码:

var a = function(){
  console.log("a1")
}
a();  // a1
var a = function(){
  console.log("a2")
}
a();  // a2

再看下面的代码:

function a(){
  console.log("a1")
}
a();   // a2
function a(){
  console.log("a2")
}
a();   // a2

看过🌰,我们都知道,JavaScript代码并不是一行行执行,中间会有一个”准备工作“(代码的预解析),比如第一段代码中的变量提升和第二段代码中的函数提升。

执行上下文类型

JavaScript中有三种类型的执行上下文:

  • 全局执行上下文:这是默认的上下文,任何不在函数体内的代码都在全局执行上下文中执行。它会执行两件事:创建window对象(浏览器环境中)并设置this的值等于这个全局对象。在一个程序中只会有一个全局执行上下文。
  • 函数执行上下文:在一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个(函数可以被多次调用)。每当一个新的执行上下文被创建,它就会按照定义好的顺序执行代码
  • eval代码:执行在eval函数内部的代码也会有它属于自己的执行上下文(在开发中不经常使用eval

执行上下文属性:

JavaScript引擎执行一段可执行代码的时候,会创建对应的执行上下文,对于每个执行上下文,都具有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

执行上下文栈(执行栈)

执行栈,也就是调用栈。我们都知道栈是后进先出(LIFO)的,执行栈用来存储代码运行时创建的所有执行上下文。

JavaScript引擎第一次遇到脚本代码是,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

引擎会执行哪些执行上下文位于栈顶的函数,当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个执行上下文。

我们可以用数组的形式模拟执行上下文栈的行为,定义一个执行上下文栈:

ECStack = [];

JavaScript要开始解释执行代码的时候,最先遇到的就是全局代码,所以一开始会向执行上下文栈压入一个全局执行上下文,我们先用globalContext表示它。只有当程序运行结束的时候,执行上下文栈才会被清空,所以在程序结束之前,执行上下文栈底部永远有一个globalContext

ECStack = [
  globalContext
]

我们再看下面的代码:

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

在调用函数fun1的时候,创建一个新的执行上下文并压入执行上下文栈顶部,在函数中又调用了fun2,又创建一个新的执行上下文压入执行上下文栈顶部,如此反复。直到最里层的函数fun3执行完毕,它的执行上下文就从执行上下文栈中弹出,然后又到fun2的执行上下文弹出,一直到执行完之后清空了执行上下文栈。

模拟执行上下文栈的变化如下:

// 程序开始,创建全局上下文压入栈顶
ECStack = [
  globalContext
]
// 调用了fun1 创建新的执行上下文压入栈顶
ECStack.push(<fun1> functionContext)
// fun1中调用了fun2 创建新的执行上下文压入栈顶
ECStack.push(<fun2> functionContext)
// fun2调用了fun3 创建新的执行上下文压入栈顶
ECStack.push(<fun3> functionContext)
// fun3执行完毕弹出
ECStack.pop()
// fun2执行完毕弹出
ECStack.pop()
// fun1执行完毕弹出
ECStack.pop()

思考一下

我们看如下两段代码,看他们有什么不同:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

乍一看好像没啥不同,他们都是输出local scope,我们用上面执行上下文的思路再看就能发现它们的执行上下文栈的变化是不同的

我们模拟一下第一段代码:

// 程序开始,创建全局上下文压入栈顶
ECStack = [
  globalContext
]
// 调用了checkscope 创建新的执行上下文压入栈顶
ECStack.push(<checkscope> functionContext)
// checkscope返回过程中中调用了f 创建新的执行上下文压入栈顶
ECStack.push(<f> functionContext)
// f执行完毕弹出
ECStack.pop()
// checkscope执行完毕弹出
ECStack.pop()

我们再看第二段代码:

// 程序开始,创建全局上下文压入栈顶
ECStack = [
  globalContext
]
// 调用了checkscope 创建新的执行上下文压入栈顶
ECStack.push(<checkscope> functionContext)
// checkscope执行完毕弹出
ECStack.pop()
// checkscope返回f 之后又调用了f 创建新的执行上下文压入栈顶
ECStack.push(<f> functionContext)
// f执行完毕弹出
ECStack.pop()

JavaScript中的变量对象和活动对象、执行过程

变量对象

上面我们就了解到,JavaScript每执行一点代码,就会创建对应的执行上下文,对于每个执行上下文,都有三个重要的属性:

  • 变量对象(Variable object--VO)
  • 作用域链(Scope chain)
  • this

变量对象是和执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明

因为不同执行上下文下的变量对象不同,我们来看一看全局上下文中的变量对象和函数上下文中的变量对象(活动对象)

全局上下文

全局上下文也就是全局对象,在浏览器环境中,全局对象也就是window对象,他有一下特性:

  • 可以通过this进行引用和访问

    console.log(this); // window
    
  • 全局对象是由Object构造函数实例化的一个对象

    console.log(this instanceif Object); // true
    
  • 全局变量指向全局对象

    var a = 1;
    console.log(this.a);  // 1
    

在客户端环境中,全局对象有个window属性指向自身

var a = 1;
console.log(window.a); // 1

this.window.b = 2;
console.log(this.b); // 2

活动对象

在全局执行上下文中我们定义的变量叫做变量对象,而在函数上下文中,我们用**活动对象(Activation object--AO)**来表示变量对象

变量对象和活动对象实际上是一个东西,只有在进入一个函数执行上下文的时候,这个执行上下文的变量对象才会被激活,也就是活动对象,它通过函数的arguments属性初始化

JavaScript的执行过程

整个JavaScript代码的执行过程,我们可以分为两个阶段:

  • 分析阶段:刚进入到执行上下文,这个时候还没有执行代码

    这个时候的变量对象会包括:

    • 函数的所有形参(如果是函数上下文)
      • 由名称和对应值组成的一个变量对象的属性被创建出来
      • 没有实参,属性设置为undefined
    • 函数声明
      • 由名称和对应值(函数对象)组成一个变量对象的属性被创建
      • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
    • 变量声明
      • 由名称和对应值(undefined)组成一个变量对象的属性被创建
      • 如果变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性

    举个🌰:

    function foo(a) {
      var b = 2;
      function c() {}
      var d = function() {};
    
      b = 3;
    
    }
    
    foo(1);
    

    上面的代码在分析阶段时的AO为:

    AO = {
        arguments: {
            0: 1,
            length: 1
        },
        a: 1,
        b: undefined,
        c: reference to function c(){},
        d: undefined
    }
    
  • 执行阶段:JavaScript在执行阶段会找到各个变量对象声明赋值给AO对象

    如上面的🌰,在经过执行阶段之后,AO对象被修改成:

    AO = {
        arguments: {
            0: 1,
            length: 1
        },
        a: 1,
        b: 3,
        c: reference to function c(){},
        d: reference to FunctionExpression "d"
    }
    

JavaScript中的作用域和作用域链

作用域

作用域就是你的代码在运行时,某些特定部分中的变量、函数和对象的可访问范围。换句话来说,作用域决定了变量和函数在什么地方能够访问到,即作用域控制着变量与函数的可见性和生命周期JavaScript采用了词法作用域(lexical scoping),也就是静态作用域

JavaScript中有两种作用域

  • 全局作用域
  • 局部作用域

简单来说,可以通过大括号{}来区分某个变量是否是局部作用域,如果一个变量或者函数在{}外声明,那它的作用域将属于外部,在内部也可访问,在ES6之前的局部作用域只包含了函数作用域,ES6提供的块级作用域也属于局部作用域

静态作用域和动态作用域

我们先来了解下静态作用域和动态作用域的相关概念,因为JavaScript采用的词法作用域,函数的作用域在函数定义的时候就决定了。而与词法作用域相对的动态作用域,函数的作用域是在函数调用执行的时候才决定的。举个🌰:

var a = 1;

function test1(){
  console.log(a)
}

function test2(){
  var a = 2;
  test1();
}

test2();
// 结果是??  1

因为JavaScript采用的词法作用域,即静态作用域,我们来分析下执行过程:

  1. 执行函数test2,定义变量a,执行函数test1,输出变量a
  2. 在输出a的时候先在本函数作用域查找变量a,如果没有找到就往定义当前函数的上一层级查找,也就找到a = 1

假设JavaScript采用动态作用域,执行过程将会是:

  1. 执行函数test2,定义变量a,执行函数test1,输出变量a
  2. 在输出a的时候先在本函数作用域查找变量a,如果没有找到就往调用当前函数的上一层级查找,也就找到a = 2

注意:静态作用域和动态作用域的区别也就在与静态作用域寻找变量作用域是按照定义位置查找,动态作用域寻找变量是按照调用位置查找

全局作用域

拥有全局作用域的的对象和变量可以在代码的任何一个地方访问到,在JavaScript中有以下两种情形为全局作用域:

  • 在代码最外层的函数和变量

    var aa = 1;
    function test(){
      // 内部可以访问外部
      console.log(aa)
    }
    // 定义在外部的变量外部也能访问,拥有全局作用域
    console.log(aa)
    
  • 未使用var、let、const等声明符声明直接进行赋值的变量

    function test(){
      aa = 1;
      console.log(aa)
    }
    // 直接赋值的变量默认为全局变量
    console.log(aa)
    

局部作用域

和全局作用域相反,局部作用域只能在固定的代码块中可以访问,最常见的就是函数体。

  • 函数作用域

    function test(){
      var aa = 1;
      // 函数体内部可以访问
      console.log(aa)
    }
    // 外部不能访问
    console.log(aa)
    
  • 块级作用域(ES6中的letconst可以创建块级作用域)在使用letconst的时候需要注意:

    • letconst不存在变量提升,在先使用后声明的时候会抛出异常

      console.log(aa); // 抛出异常
      let aa =1;
      for(let i=0;i<5;i++){
        console.log(i);
      }
      console.log(i);  // 抛出异常,let是块级作用域
      
    • letconstvar不同,不允许重复声明

      var aa = 1;
      var aa = 2;
      console.log(aa); // 2
      
      var bb = 1;
      let bb = 2;    // 抛出异常
      

作用域链

我们前面讨论了JavaScript代码会经过分析阶段和执行阶段,了解了代码的执行过程,我们再来看作用域链。

JavaScript在每一个函数执行时,会先在自己创建的AO对象上找对应的属性值,若找不到则往父函数的AO对象上找,如此往复,一直找到最后的对象window(全局作用域),而这一条形成的AO对象链就被叫做作用域链

我们知道,一个函数的作用域在函数定义的时候就已经决定了(JavaScript是静态类型语言),这是因为在函数中有一个内部属性[[scope]],当函数在被创建的时候,就会保存所有的父变量对象到其中,[[scope]]也就是所有父变量对象的层级链,但[[scope]]并不代表完整的作用域链

看下面这🌰:

function foo(){
  function bar(){
    
  }
}

在函数创建时,各自的[[scope]]为:

foo.[[scope]] = [
  globalContext.VO  // 全局变量对象
]
bar.[[scope]] = [
  fooContext.AO,  // 父函数执行上下文的活动对象
  globalContext.VO
]

当一个函数被执行,进入函数上下文,创建VO/AO后,就会将活动对象添加到作用域链的前端。这个时候执行上下文的作用域链我们命名为Scope

Scope = [AO].concat([[Scope]])

到这个时候作用域链创建完毕。

举个🌰:

我们刚才创建的函数:

var scope = "global scope";
function checkscope(){
  var scope2 = "local scope";
  return scope2
}
checkscope();

这个函数的执行过程如下:

  • 创建函数checkscope,保存作用域到内部属性[[scope]]

    checkscope.[[scope]] = [
      globalContext.VO
    ]
    
  • 执行函数checkscope,创建函数执行上下文,checkscope函数执行上下文被压入执行上下文栈

    ECStack = [
      checkscopeContext,
      globalContext
    ]
    
  • 函数checkscope不立刻执行,而是开始做准备工作(分析阶段):

    • 复制函数的[[scope]]属性创建作用域链

      checkscopeContext = {
        Scope:checkscope.[[scope]]
      }
      
    • 使用arguments创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

      checkscopeContext = {
        AO:{
          arguments:{
            length:0
          },
          scope2:undefined
        },
        Scope:checkscope.[[scope]]
      }
      
    • 将活动对象压入checkscope函数的作用域链顶端

      checkscopeContext = {
        AO:{
          arguments:{
            length:0
          },
          scope2:undefined
        },
        Scope:[AO,[[Scope]]]
      }
      
  • 准备工作做完之后开始执行函数,随着函数的执行,修改AO的值(执行阶段)

    checkscopeContext = {
      AO:{
        arguments:{
          length:0
        },
        scope2:"local scope"
      },
      Scope:[AO,[[Scope]]]
    }
    
  • 查找到变量scope2的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

    ECStack = [
      globalContext
    ]
    

在这里说一下,我们都知道代码的执行分为分析阶段执行阶段,在分析阶段走完之后,会把AO(活动对象)给执行阶段,这时候会做一下操作:

  • 引擎询问作用域,作用域中是否有某变量
  • 如果作用域中有这个变量,引擎就会使用这个变量
  • 如果作用域中没有这个变量,引擎会沿着作用域链向上层作用域查找,查找到全局作用域还是没找到变量,就抛出错误

比如上面我们的函数checkscope,函数中返回了变量scope2,引擎将在本函数作用域中查找这个变量,找不到就去上一层作用域找(全局作用域),找不到才会跑出错误

寻找变量的过程

我们都知道引擎在使用变量的时候会沿着作用域链在作用域上进行查找,在找的过程中就有LHSRHS查询,引用《你不知道的JavaScript(上)》的解释:

LHS = 变量赋值写入内存;RHS = 变量查找或从内存中读取

LHS和RHS的特性:

  • 都会在所有作用域中查询
  • 严格模式下,找不到所需变量时,引擎会抛出ReferenceError异常
  • 非严格模式下,LHS会自动创建一个全局变量
  • 查询成功时,如果对变量的值进行不合理的操作(对一个非函数的值进行函数调用操作),引擎会抛出TypeError异常

flex-grow,flex-shrink,flex-basis