likes
comments
collection
share

《JavaScript高级程序设计》(一) ---语言基础

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

写在前面: 查漏补缺系列旨在跟随《JavaScript高级程序设计》一书对JS的内容进行复盘,向心流状态前进!

语言基础

严格模式

在ECMA5新增了严格模式的概念,是一种不同的JavaScript解析和执行模型,ECMA3中的不规范写法会进行处理,对于不安全的会话将抛出错误

启用严格模式

  • 整个脚本:

    在整个脚本第一行添加: 
    "use strict"
    
  • 单个函数

    在函数内部第一行添加: 
    "use strict"
    整个函数就会按照严格模式进行解析和处理
    

严格模式的注意点

  • this

    this默认执只指向undefined,而不是window

  • 八进制

变量

变量声明

变量声明分为三个阶段:

  1. 创建:在内存中开辟空间
  2. 初始化: 将变量初始化为undefined
  3. 赋值: 真正的赋值

var

  • 作用域: var声明的作用域范围为函数作用域

  • 变量提升: var声明的变量存在变量提升(声明语句默认提升至作用域顶部)

    • 准确的说var声明的变量创建,初始化两个阶段被提升了

let

  • 作用域: let声明的变量作用域为块级作用域(一对花括号)

  • 变量提升: let声明的变量不会在作用域中提升

    • 准确的说let声明的变量创建阶段被提升了,但初始化没有提升,由于暂时性死区的存在我们无法直观的感受到变量提升的效果
  • 同一个作用域不能重复声明

const

  • 和let类似唯一不同是: const声明时必须赋值,且赋值之后不能修改

    • const实际上保证的是变量的引用地址不变,因此const声明的引用数据类型是可以修改变量的属性或元素的

声明的最佳实践

const一把梭,梭不下去了,回头改成let,绝不用var

数据类型

Null类型

null值表示的是一个空对象的指针,这也是为什么typeof null返回object的原因,在你不知道的javascript一书中是这样解释的: 不同的对象在底层都表示为二进制,在javascript中二进制的前三位都为0的就会判断为object,null的二进制表示全是0因此typeof null返回object

Number类型

js使用IEEE 745格式表示整数和浮点数,证实使用了这种格式才会出现0.1+0.2 !== 0.3的情况,所有使用这种格式的语言都存在这个问题

  • 二进制: 以'0b'开头表示二进制,将二进制字符串转换为Number: Number('0b' + str)
  • 十进制
  • 十六进制: 以'0x'开头表示十六进制,将二进制字符串转换为Number: Number('0x' + str)
  • 八进制: 开头必须是0,严格模式下,前缀0被识别为语法错误,后面的数值超过应用的范围会以十进制进行识别,es6中八进制通过0o前缀进行识别
浮点数:
  • 由于浮点数使用的内存空间是整数的两倍,js在存储浮点数时总是优先转换成整数进行存储

    • 1.0 => 小数点后面没有数字,当成1来处理
    • 10.0 => 同上当作10来处理
  • 浮点数精度最高17位,远不如整数精确

  • 科学计数法使用e来表示,前面是数值,后面是次幂: let floatNum = 3.12e103.12乘以10的10次方

值的范围
  • 最大值: Infinity
  • 最小值: -Infinity
  • 要确定一个数字是不是有限大可以使用isFinite() ,超出范围返回false
类型转换

使用Number(),parseInt(),parseFloat()将非Number类型转换为Number

  • Number()转换规则

    • 布尔: true转换为1,false转换为0

    • 数值: 直接返回

    • undefined: 返回NaN

    • null: 返回0

    • 字符转

      • 如果是包含有效的数值字符的字符串(包含前面的正负号),忽略前面的0转换成十进制
      • 如果是包含有效的十六进制格式的字符,转换成16进制对应的十进制
      • 空字符: 返回0
      • 如果包含上述情况之外的字符,返回NaN
    • 对象: 先调用valueOf()方法,并按照上述规则进行转换,若转化结果为NaN,则调用toString()方法再将结果按照上述规则转换并返回

  • parseInt()转换规则

    parseInt转换规则更专注于字符串是否包含数值模式,同时会忽略前面的空格,从第一个非空字符按照下面的规则开始转换

    • 若不是数值,正好,负号,返回NaN

    • 若是数值,则继续转换第二个字符,知道遇到非数值或者转换完毕,返回转换好的整数

    • 同时parseInt()接受第二个参数来指定进制数(大于1小于36),会按照进制数进行转换,默认为十进制,超过范围返回NaN

      • 经典面试题: 
        [1,2,3].map(parseInt) 输出 [1, NaN, NaN]
        过程如下: 
        实际上parseInt的参数是这样的: 
         [1, 0, Array(3)]
         [2, 1, Array(3)]
         [3, 2, Array(3)]
        执行情况是这样: 
        parseInt(1,0) 进制数为0,十进制,返回1
        parseInt(2,1)进制数为1,不在范围,返回NaN
        parseInt(3,2)进制数为2,但3不是二进制数字,无法转换返回NaN
        
  • parseFloat()转换规则

    与parseInt()转换规则相似,但是其没有第二个参数

String类型

ECMAScript中的字符串是不可以改变的,一旦创建,后续修改的时候必须先销毁原始字符串,然后将新值保存到变量中,这也是早期(IE6)浏览器处理字符串拼接慢的原因

转换字符转
  • toString()方法

    • 几乎所有的值都有这个方法,null和undefined除外,其作用就是返回当前值的字符串等价物
    • 在对数值掉调用这个方法的时候,可以接受一个底数作为参数: 表示以什么底数输出数值的字符串表示,2进制?十进制? 八进制?等,m默认是十进制
  • String()

    转换规则如下

    • 如果值有toString方法,则调用该方法并返回结果
    • 如果是null,返回'null'
    • 如果是undefined,返回'undefined'
模板字符串
  • 标签函数

    模板字符串的高级用法,将一个普通函数以前缀在模板字符串之前的方式,来执行自定义的插值行为,

    标签函数接收的第一个参数是原始字符串数组(插值为空字符串),其余参数是对每个插值表达式的求值结果

    const simpleTag = (strs, ...arr) => {
      console.log('arr:', arr);
      console.log('strs:', strs);
      return 'hello'    
    };
    
    const result = simpleTag`${1}${2}+${3}`;
    //输出
    //arr: [ 1, 2, 3 ]
    //strs: [ '', '', '+', '' ]
    console.log(result);
    //输出
    // hello
    
    //封装使其生成原有的表达式
    const simpleTag = (strs, ...arr) => arr.reduce((acc,curr,i) => acc + curr + strs[i+1],strs[0])
    
    • 使用场景:

      1. xss防护

        const againstXss = (strings, ...arr) =>
          arr.reduce(
            (acc, curr, i) =>
              acc + curr?.replace(/</g, '&lt;').replace(/>/, '&gt;') + strings[i + 1],//replqace more
            strings[0]
          );
        
      2. 文本高亮等

  • 获取原始字符串

    使用默认的String.raw标签函数可以获取到原始模板字面量的内容,而非转换后的

    console.log(`one\ntwo`)
    //one
    //two
    console.log(String.raw`one\ntwo`)
    //one\ntwo
    

    也可使用标签函数第一个参数,即字符串数组的.raw属性,获取到数组元素的原始内容

    const simpleTag = (strs, ...arr) => {
      strs.raw.map((item) => console.log(item));
    };
    

Symbol类型

符号,基本数据类型,唯一不可变,主要用途是确保对象属性使用唯一的标识符,使用Symbol()函数来进行初始化,函数接收一个字符串作为参数叫做符号描述,该参数只是方便开发人员辨认,与符号定义无关

const sym1 = Symbol()
const sym2 = Symbol()
sym1 === sym2 //false
//带有符号描述的符号
const sym3 = Symbol("描述")
全局符号注册表

当需要共享使用同一个符号时,可以通过Symbol.for()方法以一个字符串作为键,在全局符号注册表中创建并重用符号

const sym1 = Symbol.for("one")
const sym2 = Symbol.for("one")
console.log(typeof sym1) //Symbol
console.log(sym2 === sym1); //true

上述sym1与sym2输出为true的原理是: Symbol.for()对每个字符串执行幂等操作,同一个字符串无论执行多少次都和执行一次结果相同.第一次执行时,Symbol.for()会先检查全局运行时注册表,发现不存在这个符号就创建并添加到注册表中,否则就返回存在的

使用Symbol.keyFor()查询全局注册表,改方法接收符号返回对应的字符串键,若查询的不是全局符号返回undefined,若传递的不是符号会抛出错误:

const sym1 = Symbol.for('one');
const sym2 = Symbol.for('one');

console.log(Symbol.keyFor(sym1)); //one
使用符号作为属性

凡是字符串,数字可以作为属性的地方,都可以使用符号作为属性,对象字面量只能使用计算属性语法将符号作为属性

const sym1 = Symbol('one');
const obj1 = {
  [sym1]: 1,
};
console.log('obj1:', obj1);
//obj1: { [Symbol(one)]: 1 }

const sym2 = Symbol('two');
const obj1 = {
  sym2: 1,
};
console.log('obj2:', obj2);
//obj2: { sym2: 2 }
与符号相关的其他方法
  • Object.getOwnPropertySymbols(),获取对象实例自身的符号属性数组
  • Object.getOwnPropertyDescriptors(),获取自身包含常规属性和符号属性描述符的对象
  • Reflect.ownKeys(),获取自身对象实例两种类型的键
const sym1 = Symbol('one');
const sym2 = Symbol('two');

const obj1 = {
  [sym1]: 1,
  [sym2]: 2,
  a: 3,
};
obj1.__proto__.f = () => {
  console.log(1);
};

console.log(Object.getOwnPropertySymbols(obj1));
console.log(Object.getOwnPropertyDescriptors(obj1));
console.log(Reflect.ownKeys(obj1));

//[ Symbol(one), Symbol(two) ]
//{
//  a: { value: 3, writable: true, enumerable: true, configurable: true },
//  [Symbol(one)]: { value: 1, writable: true, enumerable: true, configurable: true },
//  [Symbol(two)]: { value: 2, writable: true, enumerable: true, configurable: true }
//}
//[ 'a', Symbol(one), Symbol(two) ]
并不会获取原型上的属性
JavaScript内置的符号属性

这些内置的符号属性用于暴露语言内部的行为,每个内置符号属性对应都有内置方法(如Symbol.hasInstance属性对应的就是instaceof方法).开发者可以直接访问,覆盖,模拟这些方法,注意这些内置符号属性是不可写,不可枚举,不可配置的

Function.prototype[Symbol.hasInstance] = () => {
return false;
}; 重写不会生效

内置属性最重要的用途就是重新定义他们,经典面试题:如何使用for-of遍历对象,就可以在自定义对象上实现Symbol.iterator的值,来改变for-of迭代对象时的行为

  • Symbol.asyncIterator

    在ECMAscript规范中,这个符号属性表示一个方法: 返回对象默认的AsyncIterator对象,由for-await-of循环使用.Array,Map,Set,String默认实现该接口

    asyncItreator是js的一个接口,用于支持异步迭代操作,该接口由[Symbol.asyncIterator]符号进行标识,由通过async function*(){}语法定义的异步生成器函数实现,异步生成器函数内部可以使用yield关键字来产生值,这些值会逐一返回给迭代器的调用方.

    • 异步生成器函数的默认返回值是生成器实例,循环时js内部会自动调用这个函数来获取生成器实例然后进行迭代,生成器函数详解请参阅Symbol.iterator内置属性部分
    要创建一个支持asyncIterator的对象,需要为这个对象定义[Symbol.asyncIterator]属性,并将该属性赋值为一个异步生成器函数,在该函数中通过yield产生值,并将值返回给迭代器的调用方.
    
    const asyncIteratorObj = {
      [Symbol.asyncIterator]: async function* () {
        yield 'hello';
        yield 'async';
        yield 'iterator';
      },
    };
    实现了这个接口就可以通过异步迭代器迭代该对象
    const autoIteration = async (obj) => {
      for await (const item of obj) {
        console.log(item);
      }
      //下面的循环与上面的循环是等价的
      //const iterator = asyncIteratorObj1[Symbol.asyncIterator]();
      //for await (const a of iterator) {
        //console.log(a);
      //}
    };
    autoIteration(asyncIteratorObj); //依次输出hello,async,iterator
    //修改以下生成器,让他每次产生的值为一个异步
    const asyncIteratorObj1 = {
      [Symbol.asyncIterator]: async function* () {
        yield new Promise((resolve) => setTimeout(() => resolve(1), 1000));
        yield new Promise((resolve) => setTimeout(() => resolve(2), 2000));
        yield new Promise((resolve) => setTimeout(() => resolve(3), 3000));
        yield new Promise((resolve) => setTimeout(() => resolve(4), 4000));
      },
    };
    autoIteration(asyncIteratorObj1);
    //1s后打印 1
    //打印1后,2s后打印2 依次类推
    由此可见我们通过定义[Symbol.asyncIterator]属性,实现了异步迭代一个对象
    
    • next()方法: 上述代码中通过异步迭代器我们实现了自动异步迭代,那如何手动进行异步迭代呢?asyncIterator接口提供了next()方法以便于我们可以手动获取下一次异步迭代的值,调用next方法返回的是一个PromisePromise会在就绪时返回一个对象{value: 值,done:是否迭代已经完成}

      const asyncIteratorObj = {
        [Symbol.asyncIterator]: async function* () {
          yield new Promise((resolve) => setTimeout(() => resolve(1), 1000));
          yield new Promise((resolve) => setTimeout(() => resolve(2), 2000));
          yield new Promise((resolve) => setTimeout(() => resolve(3), 3000));
          yield new Promise((resolve) => setTimeout(() => resolve(4), 4000));
        },
      };
      const manualIteration = async () => {
        const iterator = asyncIteratorObj[Symbol.asyncIterator]();//通过异步生成器本身获取到异步生成器实例
        iterator.next().then((data) => console.log(data));
        const res = await iterator.next();
        console.log('res:', res);
      };
      manualIteration();
      //{ value: 1, done: false }
      //res: { value: 2, done: false } 若done属性为true,标识对象已经迭代完毕,这里我们只迭代到2,还未迭代完成所以是false
      
  • Symbol.hasInstance

    根据ECMAScript规范,这个符号作为一个属性表示: 一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例,由instanceof操作符使用.这个属性定义在Function原型上,因此所有的函数和类都可以调用,instanceof操作符可以用来确定一个对象实例的原型链上是否有原型.

    class A {}
    const a = new A();
    //下面的使用方式是等价的
    console.log(a instanceof A); //true
    console.log(A[Symbol.hasInstance](a)); //true
    
    //根据原型链的特性,可以在构造函数中重覆盖[Symbol.hasInstance]方法,实现自定义的instanceof行为
    class B {
      static [Symbol.hasInstance]() {
        return false;
      }
    }
    const b = new B();
    console.log(b instanceof B); //false
    console.log(B[Symbol.hasInstance](b));//fasle
    
  • Symbol.isConcatSpreadable

    该内置属性为一个布尔值,用以配置一个对象被用作Array.prototype.concat()的参数时是否展开其元素.

    • 对于数组对象作为该参数时,默认按照数组元素展开然后进行连接.
    • 对于类数组对象作为该参数时,默认该对象整体作为新数组元素进行连接

    通过修改内置属性可以修改concat的行为

    const arr1 = [1, 2, 3];
    const arr2 = [5, 6, 7, 8];
    console.log(arr1.concat(arr2)); //[1, 2, 3, 5,6, 7, 8]
    arr2[Symbol.isConcatSpreadable] = true;
    console.log(arr1.concat(arr2)); //[1, 2, 3, 5,6, 7, 8]
    arr2[Symbol.isConcatSpreadable] = false;
    console.log(arr1.concat(arr2)); //[ 1, 2, 3, [ 5, 6, 7, 8, [Symbol(Symbol.isConcatSpreadable)]: false ] ]
    
    const obj1 = {
      a: 1,
      b: 2,
    };
    console.log(arr1.concat(obj1)); //[ 1, 2, 3, { a: 1, b: 2 } ]
    obj1[Symbol.isConcatSpreadable] = false;
    console.log(arr1.concat(obj1)); //[ 1, 2, 3, { a: 1, b: 2, [Symbol(Symbol.isConcatSpreadable)]: false } ]
    obj1[Symbol.isConcatSpreadable] = true;
    console.log(arr1.concat(obj1)); //[ 1, 2, 3 ]
    
  • Symbol.iterator

    该内置属性表示: 一个方法,方法返回对象默认的迭代器,由for-of语句使用.值为一个生成器函数.一些数据类型用于默认的迭代器行为,可以直接使用for of进行迭代: Array,String,Map,Set,其他的类型则没有: Object

    • 生成器函数

      • 生成器函数由function*语法定义,包含yeild关键字用于指定函数的暂停点,第一次调用生成器函数时不会执行函数的任何代码,而是返回一个称为Generator的迭代器,该迭代器通过next()方法来控制生成器函数的执行,
      • yeild:该关键字只能在生成器函数中使用,当生成器函数执行到yeild时,会暂停生成器函数的执行,并将yeild后面表达式的值作为生成器函数的当前暂停状态,返回给next()函数
      • next(): 是Generator迭代器的方法用于控制生成器函数的执行,迭代器调用next()时,生成器函数会执行,直到遇到yeild关键字,然后暂停,并把yeild生成的值进行返回,返回值固定为两个属性{value,done},vaule的值是yeild返回的值,done表示迭代是否完毕ture表示迭代完毕.next()方法可以接受一个参数,该参数会成为生成器函数中上一个 yield 表达式的返回值。
    • 实现可迭代对象

      let obj2 = {
        name: 'qqq',
        age: 12,
        sex: '男',
      };
      obj2[Symbol.iterator] = function* () {
        let index = 0;
        let arr = Object.keys(obj2);
        while (index < arr.length) {
          yield arr[index];
          index++;
        }
      };
      for (const key of obj2) {
        console.log(key);
      }
      
  • Symbol.match

    这个符号作为属性表示: 一个正则表达式方法,该方法用正则表达式去匹配字符串,由String.prototype.match()方法使用.String.prototype.match()会使用以[Symbol.match]为键的函数来对正则表达式求值,正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数.

    RegExp.prototype[Symbol.match] //f[Symbol.match](){[native code]}
    实际上string.match()求值的时候就是调用的而正则表达式原型上的[Symbol.match]方法
    

    该方法接收一个正则表达式为参数,若传递的式非正则表达式会导致该值转换成正则表达式再求值.可以通过重新定义Symbol.match以取代默认的行为,使match()方法使用非正则表达式实例

    class Test {
      static [Symbol.match](target) {
        return target.includes('regex');
      }
    }
    
    console.log('this is regex'.match(Test)); //true
    
  • Symbol.replace ,Symbol.search,Symbol.split与Symbol.match相同,正则表达式的原型上默认有这个些函数,调用replace,search,split方法时,实际上调用的是正则表达式原型上对应的内置属性的方法

  • Symbol.species

    表示一个函数值,该函数作为派生对象时的构造函数,主要用于控制派生对象的构造,在进行构造函数的操作时(如map ,filter)会触发该函数,确保派生对象的类型与原始对象的类型一致,,静态获取器(getter): 当访问属性时会自动调用调用该函数来返回值.用Symbol.species定义静态的获取器(getter)方法,当访问Symbol.species属性时,自动调用getter方法返回对象类型,确保类型一致性

    class B extends Array {
      static get [Symbol.species]() {
        return Array;
      }
    }
    const b = new B();
    console.log(b instanceof Array);//true
    console.log(b instanceof B); //true
    const newB = b.map(() => 1); //进行一下操作
    console.log(newB instanceof Array); //true
    console.log(newB instanceof B); //false
    
  • Symbol.toPrimitive

    该内置属性是一个方法用于控制自定义对象转换为原始值时的行为,许多内置操作都会尝试将对象转换成原始值(数字,字符串以及未指定的类型),该方法接收一个字符串参数,可以是string,number,default即对象转换的目标类型

    const obj1 = {
      [Symbol.toPrimitive](target) {
        if (target === 'string') {
          return 'string';
        }
        if (target === 'number') {
          return 1;
        }
        return 'default';
      },
    };
    const obj2 = {};
    
    console.log(Number(obj1)); //1
    console.log(Number(obj2)); //NaN
    
    console.log(String(obj1)); //string
    console.log(String(obj2)); //[object object]
    
    console.log(3 + obj1); //3default
    console.log(3 + obj2); //3[object object]
    
  • Symbol.toStringTag

    该内置属性的值是一个字符串,作为对象的默认字符串描述,由Object.prototype.toString()使用,通过toString()方法获取对象标识的时候,会检索该属性的值,默认为object

    const obj = {
      [Symbol.toStringTag]: '啥也不是',
    };
    console.log(obj.toString()); //[object 啥也不是]
    
  • Symbol.unscopables

    该属性值是一个对象,该对象的属性都会从关联对象的with环境中排除,设置这个符号并将其映射对应的属性的键值为true,就能阻止该属性出现在with环境中了,

    const obj = {foo: 'bar'}
    with(obj){
        console.log(foo) //bar
    }
    obj[Symbol.unscopables] = {
        foo: true
    }
    with(obj){
        console.log(foo) //ReferenceError
    }
    
    • with简介

      `with`JavaScript 中的一个语句,用于创建一个临时的作用域,以便在其中访问指定对象的属性,而不必使用该对象的名称前缀。尽管 `with` 在某些情况下可能方便,但它已被视为不推荐使用,并且在严格模式下是禁止的。
      `with` 语句的基本语法如下:
      with (object) {
        // 在这个作用域内可以直接访问 object 的属性
        // 不需要使用对象名称前缀
        // 例如,可以直接访问 object.property
      }
      `with` 语句的作用域中,你可以直接访问指定对象的属性,而不必在每次访问时重复指定对象的名称。这可以使代码更简洁,但也引入了一些潜在的问题,因此 `with` 被视为不安全和难以调试。
      潜在问题包括:
      1. 作用域歧义:`with` 可能会引入变量名冲突,因为在 `with` 语句块中的标识符会首先在指定对象的属性中查找,然后才在外部作用域中查找。这可能导致意外的行为。
      2. 性能问题:`with` 语句可能会导致 JavaScript 引擎在执行时难以优化代码,因此可能会导致性能下降。
      3. 调试困难:在使用 `with` 时,调试器可能无法正确显示变量的值,因为作用域被改变了。
      由于这些问题,通常不推荐使用 `with` 语句,而应该使用明确的对象引用来访问属性,以使代码更可读、可维护和可预测。在严格模式下,`with` 语句是禁用的,无法使用。
      

语句

标签语句

标签语句用于给代码块添加标签,方便代码跳转和控制循环

语法:

标签: js代码

start: const num = 0
  • 使用标签语句结合break跳出双层循环
let num = 0;
start: for (let a = 0; a <= 10; a++) {
  for (let b = 0; b <= 10; b++) {
    if (a === 0 && b === 10) {
      break start; //break只能跳转到封闭语句的标签,如for,while,do..while等
    }
    num++;
  }
}
console.log(num); //10,如果没有使用标签语句直接break,会输出120