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基础——变量、类型、操作符 #12

Open
L-small opened this issue Apr 25, 2020 · 0 comments
Open

JS基础——变量、类型、操作符 #12

L-small opened this issue Apr 25, 2020 · 0 comments

Comments

@L-small
Copy link
Owner

L-small commented Apr 25, 2020

image

JS 数据类型

基本(原始Primitive)类型:undefined, null, String, Number, Boolean, Symbol
引用(对象)类型:Object

基本类型是没有方法可以调用的。比如undefined.toString()是会报错的,'1'.toString()可以是因为'1'已经不是原始类型了。

基本包装类型(装箱拆箱)

'1'.toString()有方法调用,这主要是因为Boolean,Number,String有相应的包装类型,是为了方便操作基本数据类型设置的。

包装类型和引用类型的区别主要是对象生存期:

引用类型:使用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都保存在内存中
包装类型:只存在这一行代码执行的瞬间,然后立即销毁,所以不能在执行那一瞬间添加方法和属性。

var a = '123';
var b = a.substring(2);
第二行相当于执行了
var s1 = new String('123');
var b = a.substring(2);
s1 = null;

每当读取一个有包装类型的基本类型时,会自动创建一个对应的基本包装类型对象,从而让我们能够调用一些方法操作这些数据。

var s1 = 'some text';
s1.color = 'red';
alert(s1.color)   // undefined

因为第二行创建的对象其实在第二行执行完已经销毁了。第三行读取的时候自己又创建了String对象,然后新的对象没有color属性。

使用new String()调用和String调用是不同的,前者typeof 返回object并且可以添加属性和方法,但是容易让人不清楚是在操作基本类型还是引用类型,所以不推荐使用,后者则还是string。

var value = '123'
var number = Number(value);
typeof number  //  'number'

var obj = new Number(value);
typeof obj        //  'object';

拆箱:将引用类型转换成对应值的基本类型,是通过引用类型的valueOf方法或者toString方法来实现的,也就是JS的隐式转换

javascript中的装箱和拆箱操作

类型转换和隐式转换

转换类型只有三种情况:

  1. 转为布尔值
  2. 转为数字
  3. 转为字符串

如下图:

image

对象转原始值就是通过调用内置的ToPrimitive

ToPrimitive(obj, PreferredType) 函数转换的结果一定是基础类型

  • 当PreferredType判断为Number时(默认为Number),转换规则如下
1. 如果返回的值是基础类型,则直接返回
2. 如果传入的值是对象则采用对象的valueOf方法,如果valueOf返回值是基础类型则返回
3. 否则调用对象的toString方法,如果返回值是基础类型则返回
4. 否则抛出TypeError
  • 当PreferredType判断为String时,转换规则如下
1. 如果返回的值是基础类型,则直接返回
2. 如果传入的值是的对象则采用对象的toString方法,toString返回值是基础类型则返回
3. 否则调用对象的valueOf方法,如果返回值是基础类型则返回
4. 否则抛出TypeError

Symbol.toPrimitive方法在转原始类型的时候优先级最高

let a = {
  valueOf() {
    return 0
  },
  toString() {
    return '1'
  },
  [Symbol.toPrimitive]() {
    return 2
  }
}
1 + a // => 3

例:

const a = {
  i: 1,
  toString: function () {
    return a.i++;
  }
}
if (a == 1 && a == 2 && a == 3) {
  console.log('hello world!');
}
// hello world!

隐式转换

1、四则运算符

加法运算符:

  • 一方为字符串则转换为字符串拼接
  • 如果一方不是数字或者字符串则转换为数字和字符串

除了加法运算符,其中一方是数字则另一方转换为数字

例:

1 + '1'  // '11'
1 + [1,2,3]   // '11,2,3'
'a' + +'b'   // aNaN

++[[]][+[]]+[+[]]==10
其中涉及到了JS的[操作符优先级](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence)跟看操作符的优先级

({} + {}) = ?
两个对象的值进行+运算符,肯定要先进行隐式转换为原始类型才能进行计算。
1、进行ToPrimitive转换,由于没有指定PreferredType类型,{}会使默认值为Number,进行ToPrimitive(input, Number)运算。
2、所以会执行valueOf方法,({}).valueOf(),返回的还是{}对象,不是原始值。
3、继续执行toString方法,({}).toString(),返回"[object Object]",是原始值。
故得到最终的结果,"[object Object]" + "[object Object]" = "[object Object][object Object]"

2、比较运算符

  • 如果是对象则通过toPrimtive转换对象
  • 如果是字符串,就根据unicode字符索引来比较

** 3、==运算符

规则如下

比较运算 x==y, 其中 x 和 y 是值,返回 true 或者 false。这样的比较按如下方式进行:

1、若 Type(x) 与 Type(y) 相同, 则

    1* 若 Type(x) 为 Undefined, 返回 true。
    2* 若 Type(x) 为 Null, 返回 true。
    3* 若 Type(x) 为 Number, 则
  
        (1)、若 x 为 NaN, 返回 false。
        (2)、若 y 为 NaN, 返回 false。
        (3)、若 x 与 y 为相等数值, 返回 true。
        (4)、若 x 为 +0 且 y 为 −0, 返回 true。
        (5)、若 x 为 −0 且 y 为 +0, 返回 true。
        (6)、返回 false。
        
    4* 若 Type(x) 为 String, 则当 x 和 y 为完全相同的字符序列(长度相等且相同字符在相同位置)时返回 true。 否则, 返回 false。
    5* 若 Type(x) 为 Boolean, 当 x 和 y 为同为 true 或者同为 false 时返回 true。 否则, 返回 false。
    6*  当 x 和 y 为引用同一对象时返回 true。否则,返回 false。
  
2、若 x 为 null 且 y 为 undefined, 返回 true。
3、若 x 为 undefined 且 y 为 null, 返回 true。
4、若 Type(x) 为 Number 且 Type(y) 为 String,返回比较 x == ToNumber(y) 的结果。
5、若 Type(x) 为 String 且 Type(y) 为 Number,返回比较 ToNumber(x) == y 的结果。
6、若 Type(x) 为 Boolean, 返回比较 ToNumber(x) == y 的结果。
7、若 Type(y) 为 Boolean, 返回比较 x == ToNumber(y) 的结果。
8、若 Type(x) 为 String 或 Number,且 Type(y) 为 Object,返回比较 x == ToPrimitive(y) 的结果。
9、若 Type(x) 为 Object 且 Type(y) 为 String 或 Number, 返回比较 ToPrimitive(x) == y 的结果。
10、返回 false。


[] == !{}
1. 由于!的优先级更高,所以先!{},进行toPrimitive,没有指定PerferredType,所以默认是number,调用toStirng返回'[object Object]',!'[object Object]'则是false,
2. 变为[] == false,所以进行[] == toNumber(false) = 0
3. [] == 0,对[]进行toPrimitive,[] = [].toString() = '',
4. '' == 0, toNumber('') = 0,
4. 所以结果是true

var a = {
  valueOf: function () {
     return 1;
  },
  toString: function () {
     return '123'
  }
}
true == a // true;
首先,x与y类型不同,x为boolean类型,则进行ToNumber转换为1,为number类型。
接着,x为number,y为object类型,对y进行原始转换,ToPrimitive(a, ?),没有指定转换类型,默认number类型。
而后,ToPrimitive(a, Number)首先调用valueOf方法,返回1,得到原始类型1。
最后 1 == 1, 返回true。

类型判断

1、typeof

typeof 1    // 'number'
typeof '1'  // 'string'
typeof true   // 'boolean'
typeof null  // 'object'
typeof undefined  // 'undefined'
typeof Symbol()  // 'symbol'
typeof new Object()  // 'object'
typeof new Function() // 'function'
typeof []   // 'object'

优点:可以快速检测除了null的基本类型
缺点:无法检测null、object、Array等类型

2、instanceof

[] instanceof Array    // true
[] instanceof Object   // true
[] instanceof window.frames[0].Array  // false

优点:只能可以检测array,function,object等引用类型,不能检测基本类型
缺点:由于instanceof是判断是否在原型链上,不能判断对象具体的类型,只能判断两个对象是否是实例关系。多全局对象(跨窗口或者多iframe)

3、Object.prototype.toString

Object.prototype.toString.call(1)  // "[object Number]"
function Aaa() {}
const obj = new Aaa();
Object.prototype.toString.call(obj)  // '[object Object]'

内部实现步骤:

1. 获取当前对象的[[class]]属性(ES6中没有[[class]],调用[[NativeBrand]])
2. 计算出'[ object' +步骤一的结果 +']'
3. 返回结果

为什么不是toString(),而要Object.prototype.toString(),是因为其他类型作为Object实例可能重写了toString方法

优点:除了自定义对象基本都能检测,即使是多全局环境
缺点:无法区分自定义对象类型,自定义类型可以采用instanceof区分

4、constructor

({a:1}).constructor === Object; // true
([1,2]).constructor === Array; // true
(new Date(2018)).constructor === Date; // true
(function foo() {}).constructor === Function; // true
('1').constructor === String; // true
(1).constructor === Number; // true
(true).constructor === Boolean; // true

缺点:undefined和null 没有constructor,而且constructor指向可以改变

JS对类型的判断

typeof原理,instanceof原理

typeof null = 'object'

  • typeof原理:typeof是通过侦测js在底层储存变量时,在变量机器码中保存的1-3位类型信息来判定返回值的
000: object
001: int
010: float
100: string
110: boolean

由于null的也是0,所以返回的是object。

  • instanceof原理:利用原型链判断“父级”的原型(prototype)对象是否在“实例”的原型链上
    image
function _instanceof(left, right){
  while(true) {
    if(left.__proto__ === null) {
      return false
    }
    if(left.__proto__ === right.prototype) {
      return true
    }
    left = left.__proto__
  }
}

undefined和null的区别

null 表示没有对象,此处不应该有值
undefined表示缺少值,此处应该有值但是还没有定义

Object.getPrototypeOf(Object.prototype)  // null
var i;   // undefined
function f(x){ console.log(x) }  // undefined

Number精度问题

JS采用了IEEE754格式来表示。IEEE754规定,双精度浮点数尾数部分的有效数字是52位。如图:
image

IEEE754规定,在计算机内部保存有效数字时,默认第一位总是1,所以舍去,只保留后面的部分。比如保存1.01时,只保存01,等读取的时候再把第一位加上,所以52位有效数字可以存储到53位。

2**53        是:符号位:0  指数位:10000110100  尾数:1.0000..000(小数点后52个0)
2**53-1     是:符号位:0  指数位:10000110100  尾数:1.11111..1111(小数点后面52个 1)
2**53-2     是:符号位:0  指数位:10000110100  尾数:1.11111..1110(小数点后面51个1,1个0)
依次类推
1                 是:符号位:0  指数位:01111111111          尾数:1.0000...001
0                 是:符号位:0  指数位:00000000000    尾数:0.0000..000

指数位第一位为1而不是0是怕被当做无效片段舍弃,所以将指数转化为10进制时要减去1023
所以能提供的安全数字是(-253, 253),注意两边都是开区间。
安全整数:“安全”意思是说能够one-by-one表示的整数,也就是说在(-2^53, 2^53)范围内,双精度浮点数和整数是一对一的,反过来说,在这个范围以内,所有的整数都有唯一的浮点数表示,这叫做安全整数。比如1 === 1.00是true。
不安全整数:而超过这个范围,会有两个或更多整数的双精度表示是相同的;反过来说,超过这个范围,有的整数是无法精确表示的,只能round到与它相近的浮点数(说到底就是科学计数法)表示,这种情况下叫做不安全整数。比如253 === 253 + 1是true。和253是一样的尾数。
最大安全整数:2
*53-1 = 9007199254740991
最大数字:则是全填满

IEEE754转换
JS中最大安全整数
双精度浮点数
IEEE754

再来看:

0.1+0.2 != 0.3

精度损失主要出现在进制转换和对阶计算中。在运算的过程中,先将0.1和0.2转换成二进制,但是他俩的二进制会无限循环

0.1 -> 0.0001100110011001...(无限循环)
0.2 -> 0.0011001100110011...(无限循环)

为什么0.1=0.1?
IEEE754针对0.1这种无限循环这种默认舍入模式舍入到最接近且可以表示的值,当存在两个一样接近的时,取偶数值。所以会有

1.35.toFixed(1) // 1.4 正确
1.335.toFixed(2) // 1.33 错误
1.3335.toFixed(3) // 1.333 错误
1.33335.toFixed(4) // 1.3334 正确
1.333335.toFixed(5)  // 1.33333 错误
1.3333335.toFixed(6) // 1.333333 错误

js尾数是52位,再加上省略一位是53位,所以2**52 = 9007199254740992。长度是16位,由于第一位默认是1,所以JS最大精度范围是17位?有疑问。

0.1.toPrecision(16) = 0.1000000000000000
0.1.toPrecision(17) = 0.10000000000000001

由于精度范围是17位,所以0.1000000000000000 = 0.1。
0.1+0.2 = 0.30000000000000004
所以0.1 + 0.2 != 0.3

解决方法:

超大整数:
1.使用bigInt处理:在整数后面加n的形式定义

const alsoHuge = BigInt(9007199254740991);       // 9007199254740991n
typeof 1n === 'bigint';         // true
const expected = 4n / 2n;   // 2n
const rounded = 5n / 2n;     // 2n
0n === 0      //  false
0n == 0        //  true

2.将数字作为字符串来处理

浮点数:
1、乘最小数倍数转为整数处理
2、字符串处理

变量在内存中的存储形式

基本类型:存储在栈内存中,值有固定大小,按值访问
引用类型:存储在堆内存中,值大小不固定,按址访问,操作对象时操作的是对象的引用。

栈的运算速度比堆快,所以将变量都放在栈中,然后引用类型的指针也放在栈中,由于引用类型结构复杂且可扩展,放在栈里影响栈性能,所以放在堆里。

所以先从栈里查找再去堆里查找

JS内存机制及参数传递

JS对象的底层存储

Object存储和查找一个属性,大概有以下几种方式
image
属性不超过128个的时候则使用Search Cache
属性都是连续数字的时候使用数组,此方法最快
其他情况使用哈希表,并且数字和字符串哈希不一样

从Chrome源码看JS Object的实现

数组和链表

严格意义上的数组,数组是一串连续的内存位置,然后通过位置偏移来获取数据。
image
JS的数组是哈希映射,可以用不同的数据结构实现,比如链表。所以JS数组获取数据则是通过遍历寻址。效率更低。
image
其实在解析的时候有优化,将同类型数据的数组JIT会转换为连续的内存地址存储,但是一旦有不同类型的数据时,则恢复到以前的哈希映射形式,效率降低。

ES6有ArrayBuffer,则是通过创建连续的内存地址,在处理大量数据时提升明显。是JS操作二进制数据的一个接口,由于直接操作二进制数据,所以可以直接和硬件进行数据交换传递,不用再进行格式转换等,所以效率高。

深入JS数组

Symbol实现原理及应用

Symbo几个特点:

  1. Symbol值通过Symbol函数生成,使用typeof,结果为"symbol"。由于typeof的原理可知,这个没法实现
  2. Symbol函数不能使用new命令,否则报错。因为它是基本类型。
  3. instanceof 结果是false
var s = Symbol('foo');
console.log(s instanceof Symbol); // false
  1. 接受一个字符串作为参数,表示对Symbol实例的描述。
var s1 = Symbol('foo');
console.log(s1); // Symbol(foo)
  1. 如果Symbol参数是一个对象的话,会调用对象的toStirng方法,将其转化为字符串,然后才生成一个Symbol值
const obj = {
  toString() {
    return 'abc';
  }
};
const sym = Symbol(obj);
console.log(sym); // Symbol(abc)
  1. Symbol函数的参数只是对当前Symbol值的描述,相同参数的Symbol函数的返回值是不相等的。
  2. Symbol值不能和其他类型值进行运算
  3. Symbol值可以显示转化为字符串
var sym = Symbol('My symbol');

console.log(String(sym)); // 'Symbol(My symbol)'
console.log(sym.toString()); // 'Symbol(My symbol)'
  1. Symbol值可以作为对象的属性,保证不会出现相同的属性
  2. Symbol值作为属性,只有Object.getOwnPropertySymbol方法可以获取指定对象所有的Symbol属性
var obj = {};
var a = Symbol('a');
var b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

var objectSymbols = Object.getOwnPropertySymbols(obj);

console.log(objectSymbols);    // [Symbol(a), Symbol(b)]
  1. 如果使用Symbol.for可以获取同一个的Symbol值,有则返回,没有就创建
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
console.log(s1 === s2); // true
  1. Symbol.keyFor方法返回一个已登记的Symbol类型值的key
var s1 = Symbol.for("foo");
console.log(Symbol.keyFor(s1)); // "foo"
var s2 = Symbol("foo");
console.log(Symbol.keyFor(s2) ); // undefined

规范中当调用 Symbol 的时候,会采用以下步骤:

如果使用 new ,就报错
如果 description 是 undefined,让 descString 为 undefined
否则 让 descString 为 ToString(description)
如果报错,就返回
返回一个新的唯一的 Symbol 值,它的内部属性 [[Description]] 值为 descString

实现

(function() {
    var root = this;
    var generateName = (function(){
        var postfix = 0;
        return function(descString){
            postfix++;
            return '@@' + descString + '_' + postfix
        }
    })()
    var SymbolPolyfill = function Symbol(description) {

        if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

        var descString = description === undefined ? undefined : String(description)

        var symbol = Object.create({
            toString: function() {
                return this.__Name__;
            }
            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;
    }

    var forMap = {}

    Object.defineProperties(SymbolPolyfill, {
        'for': {
            value: function(description) {
                var descString = description === undefined ? undefined : String(description);
                return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString)
            },
            writable: true,
            enumerable: false,
            configurable: true
        },
        'keyFor': {
            value: function(symbol) {
                for(var key in forMap) {
                    return forMap[key];
                }
            },
            writable: true,
            enumerable: false,
            configurable: true
        }
    })
    root.SymbolPolyfill = SymbolPolyfill;
})()

应用

  1. 定义类的私有属性和方法
const age = Symbol();
const getAge = Symbol();
class User {
  constructor(name, sex, age) {
    this.name = name
    this.sex = sex
    this[age] = age
    this[getAge] = () => this[age]
  }
  printAge() {
    console.log(this[getAge]())
  }
}
const u1 = new User('zhangsan', 'man', 20);
u1.name   // 'zhangsan';
u1.age      // undefined
  1. 单例模式
class Phone {
  constructor() {
    this.name = '小米',
    this.price = 1000
  }
}
let key = Symbol.for('Phone');
if(!global[key]) global[key] = new Phone()

深入理解Symbol

运算符

题一

  var a = {n:1};  
  var b = a; 
  a.x = a = {n:2};  
  alert(a.x);   // --> undefined  
  alert(b.x);   // --> { n:2 }

这题主要考的就是操作符优先级和赋值运算符
操作符优先级简单点的如下
image
赋值运算符:
像A=B这个操作规范是如何解释的:

  1. 计算表达式A,得到A的地址
  2. 计算表达式B,得到B的值
  3. 将B的值赋值给A
  4. 返回B的值
    从以上步骤可以看出,赋值是从右向左,但是读取数据是从左向右的。
    再来看A=B=C,可以看成A=(B=C)
  5. 计算表达式A,得到A的地址
  6. 计算表达式B,得到B的地址
  7. 计算表达式C,得到C的地址,
  8. 取出C的值
  9. 将C的值赋值给B,返回C的值
  10. 将C的值赋值给A,返回C的值

所以刚才那道题计算过程如下

  1. 计算表达式a.x,由于没有x属性,则新建x属性,所以a.x为undefined,得到地址为ref1
  2. 计算表达式a,得到地址ref2
  3. 将{n:2}的值的地址赋值给a,此时a的地址值(ref2)变为{n:2}的指针。由于b的值中指针还是以前a的地址
  4. 将{n:2}的值的地址赋值给a.x,此时ref1的值变为{n:2}的值的指针。
    此时的关系如下图:
    image

由ECMA规范来看连续赋值

题二

再看一题

var a = 1;
function b() {
  console.log(this.a)
}
var obj = {
  a: 2,
  fn: function() {
    console.log(this.a)
  }
}
var d = {
  a: 3
}
var c = obj.fn;
obj.fn()   // 2
c()    // 1
(d.fn = obj.fn)()    // 1
(b, obj.fn)()      // 1

这里除了this外,还考察了赋值运算符和,运算符
上一题讲了赋值运算符在赋值完之后还会返回对应的值,所以(d.fn = obj.fn)实际是上返回了obj.fn的值,所以实际上是在window环境下执行的,所以this指向window
,运算符则是返回最后一个值,所以var e = (1,2,3,4,5)时,e的值是5。所以(b, obj.fn)实际上返回的是obj.fn的值,所以也是在window环境下执行的,所以this指向window

跨域

跨域是由于浏览器的同源策略导致的。同源策略是为了防CSRF攻击
分为:
DOM同源策略:不同域的iframe限制互相访问
XmlHttpRequest同源策略:禁止使用XHR对象向不同源的服务器地址发起HTTP请求

不同源(协议,域名,端口)

没有XmlHttpRequest同源策略时的CSRF:

  1. 当你访问一个bank.com页面,bank.com将用户信息存入cookie
  2. 你访问一个恶意网站,执行一段代码获取cookie中的用户信息向bank.com中请求,你的账户就受到危险

没有DOM同源策略时的CSRF:

  1. 当坏人通过一个iframe将bank.com页面嵌套进来,你输入账号密码登录时就都暴露了。

跨域的解决方式:

  1. CORS跨域
    客户端设置,如果要携带cookie则需要添加
xhr.withCredentials = true;

服务端设置 response header中设置

Access-Control-Allow-Origin: http://www.yourhost.com
Access-Control-Allow-Credentials:true
  1. JSONP跨域
    基本原理就是通过动态创建script标签,然后利用src属性进行跨域。
  2. 服务器代理
    浏览器有同源策略,但是服务器没有,可以通过服务器代理所要域的请求
  3. document.domain
    适用于iframe 通过document.domain将两个iframe设置相同的域名,然后通过Window['iframename']获取
  4. window.name
    在一个window的生命周期内, window载入的所有的页面都是共享一个window.name的,即使页面甚至域名都不同。每个页面对window.name都有读写的权限,并且name长度可以达到2MB
  5. location.hash
    子框架可以修改父框架的src的hash值,且修改hash值父框架不会刷新。传递数值有限
  6. postMessage
  7. WebSocket
@L-small L-small changed the title JS面试题 JS基础面试题 Apr 27, 2020
@L-small L-small changed the title JS基础面试题 JS基础——变量、类型、操作符面试题 Apr 28, 2020
@L-small L-small changed the title JS基础——变量、类型、操作符面试题 JS基础——变量、类型、操作符 May 12, 2020
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