Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS变量和类型 —— 一名【合格】前端工程师的自检清单答案整理 #1

Open
akeymo opened this issue Nov 26, 2019 · 0 comments

Comments

@akeymo
Copy link
Owner

akeymo commented Nov 26, 2019

JavaScript规定了几种语言类型

String、Number、Boolean、Null、Undefined、Object、Symbol(ES6)

JavaScript对象的底层数据结构是什么

  • 每个JS对象都有一个隐藏类与之关联,在V8中,位于堆内存并由GC管理的所有JS对象的第一个字段都指向隐藏类。隐藏类存储中包含属性的数量,和一个指向描述符数组的指针。在这个描述符数组中包含有命名属性的信息,例如命名属性的名称和存储属性值的位置。隐藏类是动态创建的,并随着对象的变化而动态更新。具有相同结构的JS对象(相同顺序和相同命名的属性),他们的隐藏类会指向同一个,以此达到复用的目的。对于不同结构的JS对象将使用不同的HiddenClass
    image

  • 有一个块是properties,用来存储命名属性

  • 有一个块是elements,用来存储数组索引属性

Symbol类型在实际开发中的应用、可手动实现一个简单的Symbol

  1. 应用:
  • Symbol定义的属性会被JSON.stringify()排除在输出内容之外,也不能通过Object.keys()for...in来枚举,Object.getOwnPropertyNames(obj)也获取不到,只有Object.getOwnPropertySymbols()可以获取,因此我们可以模拟私有方法,为对象定义一些非私有的、但又希望只用于内部的方法。
     const width = Symbol('width');
     class Square {
         constructor(width) {
             this[width] = width;
         }
     
         getWidth() {
             console.log(this[width]);
         }
     }
    
  • Symbol值可以作为标识符,用于对象的属性名,可以保证不会出现同名的属性
     let mySymbol = new Symbol();
     
     // 第一种写法
     let a = {};
     a[mySymbol] = 'hello';
     
     // 第二种写法
     let a = {
     	[mySymbol]: 'hello';
     }
     
     // 第三种写法
     let a = {};
     Object.defineProperty(a, mySymbol, {value: 'hello'});
    
  • 创建枚举类型
  • 使用Symbol来代替常量,在常量多的情况下,可以保证常量的值是唯一的
     const TYPE_AUDIO = Symbol();
     const TYPE_IMAGE = Symbol();
    
  • 全局Symbol注册表,在存在多个window(例如存在iframe)但需要共享Symbol,那么需要Symbol.for()来注册或获取一个window间全局的Symbol实例
     let gs1 = Symbol.for('global_symbol_1');  //注册一个全局Symbol
     let gs2 = Symbol.for('global_symbol_1');  //获取全局Symbol
     
     gs1 === gs2;  // true
    
  1. 实现:参考
    (function () {
        var root = this;
    
        // 保证不出现同名属性
        var generateName = (function () {
            var postfix = 0;
            return function (descString) {
                postfix++;
                return '@@' + descString + '_' + postfix;
            }
        })();
    
        var SymbolPolyfill = function Symbol(description) {
            // 实现Symbol函数不能使用new命令
            if (this instanceof SymbolPolyfill) {
                throw new TypeError('Symbol is not a constructor');
            }
    
            // 如果Symbol的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个Symbol值
            var descString = description === undefined ? undefined : String(description);
    
            // 当调用String方法的时候,如果该对象有toString方法,就会调用该toString方法
            // 但因为与保证不存在同名属性的实现相矛盾,修改了
            var symbol = Object.create({
                toString: function () {
                    //return 'Symbol(' + this.__Description__ + ')';
                    return this.__Name__;
                },
                // 显式调用valueOf方法,会直接返回该Symbol值
                // 隐式调用会报错,暂时无法实现
                valueOf: function () {
                    return this;
                }
            });
    
            Object.defineProperties(symbol, {
                '__Description__': {
                    value: descString,
                    writable: false,
                    enumerable: false,
                    configurable: false
                },
                '__Name__': {
                    value: generateName(descString),
                    writable: false,
                    enumerable: false,
                    configurable: false
                }
            });
    
            // 因为调用该方法,返回的是一个新对象,两个对象之间,只要引用不同,就不会相同
            return symbol;
        }
    
        // Symbol.for接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。
        // Symbol.keyFor方法返回一个已登记的Symbol类型值的key
        var forMap = {};
        Object.defineProperties(SymbolPolyfill, {
            'for': {
                value: function (description) {
                    var descString = description === undefined ? undefined : String(description);
                    return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(description);
                },
                writable: true,
                enumerable: false,
                configurable: true
            },
            'keyFor': {
                value: function(symbol){
                    for(var key in forMap){
                        if(forMap[key] === symbol) return key;
                    }
                },
                writable: true,
                enumerable: false,
                configurable: true
            }
        });
    
        root.SymbolPolyfill = SymbolPolyfill;
    })();
    

JavaScript中的变量在内存中的具体存储形式

JavaScript中的变量分为__基本类型__和__引用类型__。

  • 基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问。
  • 引用类型是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址,指向堆内存中的对象,JavaScript不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用。
    image

基本类型对应的内置对象,以及他们之间的装箱拆箱操作

  1. 对应的内置对象
  • number -> Number
  • boolean -> Boolean
  • string -> String
  1. 装箱操作:所谓装箱,就是把基本类型转变为对应的对象。每当读取一个基本类型的值时,后台会创建一个该基本类型所对应的对象。在这个基本类型上调用方法,其实是在这个基本类型对象上调用方法。这个基本类型的对象是临时的,它只存在于方法调用那一行代码执行的瞬间,执行方法后立刻被销毁
  2. 拆箱操作:把对象转变为基本类型的值。拆箱过程内部调用了抽象操作 ToPrimitive。通过引用类型的valueOf()或者toString()方法来实现的。

理解值类型和引用类型

  1. 值类型
  • 占用空间固定,保存在栈中。
  • 保存与复制的是值本身。
  • 使用typeof检测数据类型。
  • 基本类型数据都是值类型。
  1. 引用类型
  • 占用空间不固定,保存在堆中。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象可能还被另一个引用变量所引用。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。
  • 保存与复制的是指向对象的一个指针
  • 使用instanceof检测数据类型
  • 使用new方法构造出的对象是引用型

null和undefined的区别

  • null表示的是空值即该处不应该有值,undefined表示的是未赋值即此处应该有值但还没有定义
  • null转换为数值时为0,undefiend转换为数值时为NaN

至少可以说出三种判断JavaScript数据类型的方式,以及他们的优缺点,如何准确的判断数组类型

  1. 判断数据类型的方式
    image
  • typeof,可以用来判断基本数据类型(除了null),但是对于判断引用类型就有点不适用,因为它基本都只会返回'object',无法判断具体的类型。有些例外,例如typeof null === 'object'typeof function a(){} === 'function'typeof NaN === 'number',未声明的变量,typeof照样返回'undefined'
  • instanceof,判断对象和构造函数在原型链上是否有关系。无法检测基本数据类型
  • constructor,查看对象对应的构造函数。constructor在其对应对象的原型下面,是自动生成的。当我们写一个构造函数的时候,程序会自动添加:构造函数名.prototype.constructor = 构造函数名。但是nullundefined无法判断,因为是无效的对象。使用constructor是不保险的,因为constructor属性是可以被修改的,会导致检测出的结果不正确
     var str = 'hello';
     alert(str.constructor == String);//true
     var bool = true;
     alert(bool.constructor == Boolean);//true
     var num = 123;
     alert(num.constructor == Number);//true
     // var nul = null;
     // alert(nul.constructor == Object);//报错
     // var und = undefined;
     // alert(und.constructor == Object);//报错
     var oDate = new Date();
     alert(oDate.constructor == Date);//true
     var json = {};
     alert(json.constructor == Object);//true
     var arr = [];
     alert(arr.constructor == Array);//true
     var reg = /a/;
     alert(reg.constructor == RegExp);//true
     var fun = function () { };
     alert(fun.constructor == Function);//true
     var error = new Error();
     alert(error.constructor == Error);//true
    
  • Object.prototype.toString,返回toString运行时this指向的对象类型,返回的类型格式为[object xxx]xxx是具体的数据类型。基本上所有对象的类型都可以通过这个方法得到。
     var str = 'hello';
     console.log(Object.prototype.toString.call(str));//[object String]
     var bool = true;
     console.log(Object.prototype.toString.call(bool))//[object Boolean]
     var num = 123;
     console.log(Object.prototype.toString.call(num));//[object Number]
     var nul = null;
     console.log(Object.prototype.toString.call(nul));//[object Null]
     var und = undefined;
     console.log(Object.prototype.toString.call(und));//[object Undefined]
     var oDate = new Date();
     console.log(Object.prototype.toString.call(oDate));//[object Date]
     var json = {};
     console.log(Object.prototype.toString.call(json));//[object Object]
     var arr = [];
     console.log(Object.prototype.toString.call(arr));//[object Array]
     var reg = /a/;
     console.log(Object.prototype.toString.call(reg));//[object RegExp]
     var fun = function () { };
     console.log(Object.prototype.toString.call(fun));//[object Function]
     var error = new Error();
     console.log(Object.prototype.toString.call(error));//[object Error]
    
  1. 准确判断数组类型的方法
  • Object.prototype.toString.call(arr)

可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用

  1. 字符串和数字之间
  • +的运算,如果某一个或者某两个操作数是字符串,或者可以通过步骤得到字符串,则会进行字符串拼接,否则进行数字加法。应用:利用加空字符串来将数字转为字符串
  • -的运算,是数字运算,所以会转换成数字进行运算。对象会先转换成字符串,再转换成数字。应用:利用减去0来将字符串转为数字
  1. 布尔值到数字:判断有且仅有一个参数为true的时候,可以利用将真值转为数字1,并累加,最终判断合计值是否为1来处理
  2. 非布尔值转换为布尔值
  • if(...)语句中中的条件判断
  • for(...;...;...)语句中的条件判断表达式(第二个)
  • while(...)do...while(...)循环中的条件判断表达式
  • ? :中的判断表达式
  • 逻辑运算符 ||&&左边的操作数(作为条件判断表达式)

出现小数精度丢失的原因,JavaScript可以存储的最大数字、最大安全数字,JavaScript处理大数字的方法、避免精度丢失的方法

  1. 出现小数精度丢失的原因:因为计算时会先转换成二进制,而浮点数无法精确表示其数值范围内的所有数值,只能精确表示可用科学计数法m*2^e表示的数值而已
  2. 最大数字:Number.MAX_VALUE,接近于1.79E+308
  3. 最大安全整数:Number.MAX_SAFE_INTEGER:2^53 - 1
  4. 处理大数字的方法:
  • 转换成字符串
  • chrome 67+开始支持BigInt类型,使用时在数字后面增加一个n
  1. 避免精度丢失的方法:
  • 使用第三方类库例如math.js或decimal.js等
  • toFixed()方法:四舍五入为指定位数的小数,结果不可靠
  • ES6在Number对象上新增了一个极小的常量Number.EPSILON,如果误差能够小于Number.EPSILON,我们就可以认为结果是可靠的
     function withinErrorMargin (left, right) {
         return Math.abs(left - right) < Number.EPSILON
     }
     withinErrorMargin(0.1+0.2, 0.3)
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant