Description
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的隐式转换
类型转换和隐式转换
转换类型只有三种情况:
- 转为布尔值
- 转为数字
- 转为字符串
如下图:
对象转原始值就是通过调用内置的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指向可以改变
typeof原理,instanceof原理
typeof null = 'object'
- typeof原理:typeof是通过侦测js在底层储存变量时,在变量机器码中保存的1-3位类型信息来判定返回值的
000: object
001: int
010: float
100: string
110: boolean
由于null的也是0,所以返回的是object。
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位。如图:
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
最大数字:则是全填满
再来看:
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对象的底层存储
Object存储和查找一个属性,大概有以下几种方式
属性不超过128个的时候则使用Search Cache
属性都是连续数字的时候使用数组,此方法最快
其他情况使用哈希表,并且数字和字符串哈希不一样
数组和链表
严格意义上的数组,数组是一串连续的内存位置,然后通过位置偏移来获取数据。
JS的数组是哈希映射,可以用不同的数据结构实现,比如链表。所以JS数组获取数据则是通过遍历寻址。效率更低。
其实在解析的时候有优化,将同类型数据的数组JIT会转换为连续的内存地址存储,但是一旦有不同类型的数据时,则恢复到以前的哈希映射形式,效率降低。
ES6有ArrayBuffer,则是通过创建连续的内存地址,在处理大量数据时提升明显。是JS操作二进制数据的一个接口,由于直接操作二进制数据,所以可以直接和硬件进行数据交换传递,不用再进行格式转换等,所以效率高。
Symbol实现原理及应用
Symbo几个特点:
Symbol值通过Symbol函数生成,使用typeof,结果为"symbol"。由于typeof的原理可知,这个没法实现- Symbol函数不能使用new命令,否则报错。因为它是基本类型。
- instanceof 结果是false
var s = Symbol('foo');
console.log(s instanceof Symbol); // false
接受一个字符串作为参数,表示对Symbol实例的描述。
var s1 = Symbol('foo');
console.log(s1); // Symbol(foo)
- 如果Symbol参数是一个对象的话,会调用对象的toStirng方法,将其转化为字符串,然后才生成一个Symbol值
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
console.log(sym); // Symbol(abc)
- Symbol函数的参数只是对当前Symbol值的描述,相同参数的Symbol函数的返回值是不相等的。
Symbol值不能和其他类型值进行运算Symbol值可以显示转化为字符串
var sym = Symbol('My symbol');
console.log(String(sym)); // 'Symbol(My symbol)'
console.log(sym.toString()); // 'Symbol(My symbol)'
- Symbol值可以作为对象的属性,保证不会出现相同的属性
- 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)]
- 如果使用Symbol.for可以获取同一个的Symbol值,有则返回,没有就创建
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
console.log(s1 === s2); // true
- 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;
})()
应用
- 定义类的私有属性和方法
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
- 单例模式
class Phone {
constructor() {
this.name = '小米',
this.price = 1000
}
}
let key = Symbol.for('Phone');
if(!global[key]) global[key] = new Phone()
运算符
题一
var a = {n:1};
var b = a;
a.x = a = {n:2};
alert(a.x); // --> undefined
alert(b.x); // --> { n:2 }
这题主要考的就是操作符优先级和赋值运算符
操作符优先级简单点的如下
赋值运算符:
像A=B这个操作规范是如何解释的:
- 计算表达式A,得到A的地址
- 计算表达式B,得到B的值
- 将B的值赋值给A
- 返回B的值
从以上步骤可以看出,赋值是从右向左,但是读取数据是从左向右的。
再来看A=B=C,可以看成A=(B=C) - 计算表达式A,得到A的地址
- 计算表达式B,得到B的地址
- 计算表达式C,得到C的地址,
- 取出C的值
- 将C的值赋值给B,返回C的值
- 将C的值赋值给A,返回C的值
所以刚才那道题计算过程如下
- 计算表达式a.x,由于没有x属性,则新建x属性,所以a.x为undefined,得到地址为ref1
- 计算表达式a,得到地址ref2
- 将{n:2}的值的地址赋值给a,此时a的地址值(ref2)变为{n:2}的指针。由于b的值中指针还是以前a的地址
- 将{n:2}的值的地址赋值给a.x,此时ref1的值变为{n:2}的值的指针。
此时的关系如下图:
题二
再看一题
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:
- 当你访问一个bank.com页面,bank.com将用户信息存入cookie
- 你访问一个恶意网站,执行一段代码获取cookie中的用户信息向bank.com中请求,你的账户就受到危险
没有DOM同源策略时的CSRF:
- 当坏人通过一个iframe将bank.com页面嵌套进来,你输入账号密码登录时就都暴露了。
跨域的解决方式:
- CORS跨域
客户端设置,如果要携带cookie则需要添加
xhr.withCredentials = true;
服务端设置 response header中设置
Access-Control-Allow-Origin: http://www.yourhost.com
Access-Control-Allow-Credentials:true
- JSONP跨域
基本原理就是通过动态创建script标签,然后利用src属性进行跨域。 - 服务器代理
浏览器有同源策略,但是服务器没有,可以通过服务器代理所要域的请求 - document.domain
适用于iframe 通过document.domain将两个iframe设置相同的域名,然后通过Window['iframename']获取 - window.name
在一个window的生命周期内, window载入的所有的页面都是共享一个window.name的,即使页面甚至域名都不同。每个页面对window.name都有读写的权限,并且name长度可以达到2MB - location.hash
子框架可以修改父框架的src的hash值,且修改hash值父框架不会刷新。传递数值有限 - postMessage
- WebSocket