JavaScript基础
1. JavaScript规定了几种语言类型
基础 引用两种类型
- Null
- Undefined
- String
- Number
- Boolean
- Object
- Symbol
2. JavaScript对象的底层数据结构是什么
3. Symbol类型在实际开发中的应用,可实现一个简单的Symbol
Symbol类型是一种基础数据类型,使用typeof
检查类型时属于自己的类型symbol, Symbol类型的key是不能通过Object.keys()
后者for...in
来枚举,且未被包含在对象的属性名集合(property names)之中,所以可以利用该特性,我们可以把一些不需要对外操作和访问的属性使用Symbol来定义。
应用场景1: 使用Symbol来作为对象属性名(key)
const PROP_NAME = Symbol();
let obj = {
[PROP_NAME]: '一斤代码',
title: 'Engineer',
}
obj[PROP_AGE] = 18;
Object.keys(obj) // ['title']
for(let p in obj) {
console.log(p) // 'title'
}
Object.getOwnPropertyNames(obj); // ['title']
// 利用这一特点设计我们的数据对象,让‘对内操作’和‘对外选择性输出’变得更加优雅
JSON.stringify(obj) // {title: 'Engineer'}
// 使用Object的API
Object.getOwnPropertySymbols(obj) // [Symbol()]
// 使用新增的反射API
Reflect.ownKeys(obj) // [Symbol(), 'age', 'title']
应用场景2:使用Symbol来替代常量
没有看懂呢
const TYPE_AUDIO = Symbol()
const TYPE_VIDEO = Symbol()
const TYPE_IMAGE = Symbol()
function handleFileResource(resource) {
switch(resource.type) {
case TYPE_AUDIO:
playAudio(resource)
break
case TYPE_VIDEO:
playVideo(resource)
break
case TYPE_IMAGE:
previewImage(resource)
break
default:
throw new Error('Unknown type of resource')
}
}
应用场景3: 使用Symbol定义类的私有属性/方法
// 这个也没看懂 换个文件肯定获取不到PASSWORD了呀,既然换个文件,用普通的变量也可以呀
在文件a.js中
const PASSWORD = Symbol();
class Login {
constructor(usename, password) {
this.username = username;
this[PASSWORD] = password;
}
checkPassword(pwd) {
return this[PASSWORD] = pwd;
}
}
export default Login;
在b.js中
import Login from './a';
const Login = new Login('admin', '123456');
login.checkPassword('123456') // true
// b.js获取不到PASSWORD
login[PASSWORD]
4. JavaScript中的变量在内存中的具体存储形式
栈内存和堆内存
JavaScript中的变量分为基本类型和引用类型
基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,通过按值访问引用类型是保存在栈内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向内存中的对象,JavaScript不允许直接访问堆栈内存中的位置,因此操作对象是,实际操作对象的引用
let a1 = 0; // 栈内存
let a2 = "this is string" // 栈内存
let a3 = null; // 栈内存
let b = { x: 10 }; // 变量b存在于栈中,{ x: 10 }作为对象存在于堆中
let c = [1, 2, 3]; // 变量c存在于栈中,[1, 2, 3]作为对象存在于堆中
当我们要访问堆内存中的引用数据类型时
- 从栈中获取该对象的地址引用
- 再从堆内存中取得我们需要的数据
基本类型发生复制行为
let a = 20;
let b = a;
b = 30;
console.log(a); // 20
结合下面图进行理解:
在栈 内存中的数据发生复制行为时,系统会自动为新的变量分配一个新值,最后这些变量都是相互独立互不影响的
引用类型发生复制行为
let a = { x: 10, y: 20 }
let b = a;
b.x = 5;
console.log(a.x); // 5
- 引用类型的复制,同样为新的变量b分配一个新的值,保存在栈内存中,不同的是这个值仅仅是引用类型的一个地址指针
- 它们两个指向同一个值,也就是地址指针相同,在堆内存中访问的具体对象实际上是同一个
- 因此改变b.x时,a.x也发生了变化,这就是引用类型的特性
总结
5.基本类型对应的内置对象,以及他们之间的装箱拆箱操作
内置对象 String() Number() Boolean()
装箱和拆箱
引用类型有个特殊的基本包装类型,它包含String,Number和Boolean。
装箱:
在《javascript高级程序设计》中有这样一句话:
每当读取一个基本类型的时候,后台会创建一个对应的基本包装类型对象,从而让我们能够调用一些方法来操作这些数据(隐式装箱)
隐式装箱
let a = 'sun';
let b = a.indexof('s'); // 0
// 上面代码在后台实际步骤为:
let a = new String('sun');
let b = a.indexof('s');
a = null;
在上面的代码中,a是基本类型,它不是对象,不应该具有方法,js内部进行了一些列处理(装箱),使得它能够调用方法。在这个基本类型上调用方法,其实是在这个基本类型对象上调用方法。这个基本类型的对象是临时的,它只存在于方法调用那一行代码执行的瞬间,执行方法后立即销毁。实现机制:
- 创建String类型的一个实例
- 在实例上调用指定的方法
- 销毁这个实例
显式装箱
通过内置对象可以对Boolean、Object、String等可以对基本类型显式装箱
let a = new String('sun');
拆箱:
拆箱和装箱相反,就是把引用类型转换为基本类型的数据,通常通过引用类型的valueof()和toString()方法实现
let name = new String('sun');
let age = new Number(24);
console.log(typeof name); // object
console.log(typeof age); // object
// 拆箱操作
console.log(typeof age.valueOf()) // number
console.log(typeof name.valueOf()) // string
console.log(typeof age.toString()) // string
console.log(typeof name.toString()) // string
6.理解值类型和引用类型
7.null和undefined的区别
null:
null是js中的关键字,表示空值,null可以看作是object的一个特殊的值,表示这个对象不是有效值。
undefined:
undefined不是js中的关键字,其是一个全局变量,是Global的一个属性,以下情况会返回undefiend:
- 使用了一个未定义的变量: var i;
- 使用了一定义但未声明的变量
- 使用了一个对象属性,但该属性不存在或者未赋值
- 调用函数时,该提供的参数没有提供
function func(a){
console.log(a);
}
func();//undefined
- 函数没有返回值,默认返回undefined
var aa=func();
aa;//undefined
相同点:
都是原型类型的值,保存在栈中变量本地
不同点:
1.类型不一样:
console.log(typeof undefined); // undefined
console.log(typeof null); // object
- 转换为值时不一样: undefined为NaN,null为0
console.log(Number(undefined));//NaN
console.log(Number(10+undefined));//NaN
console.log(Number(null));//0
console.log(Number(10+null));//10
何时使用:
null当使用一个比较大的对象时,需要对其进行释放内存时,设置为null
var arr = ['aa', 'bb', 'cc'];
arr = null; // 释放指向数组的引用
8.至少可以说出三种判断JavaScript数据类型的方式,以及他们的优缺点,如何准确的判断数组类型
1. typeof
typeof是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示。包括以下7种:number、boolean、symbol、string、object、undefined、function等
typeof ''; // string
typeof 1; // number
typeof Symbol(); // symbol
typeof true; // boolean
typeof undefined; // undefined
typeof null; // object 无效
typeof []; // object 无效
typeof new Function(); // function
typeof new Date(); // object 无效
typeof new RegExp(); // object 无效
有些时候,typeof操作福会返回一些令人迷惑但技术上却正确的值:
- 对于基本类型,除null以外,均可以返回正确的结果
- 对于引用类型,除function以外,一律返回object类型
- 对于null,返回object类型
- 对于function返回function类型
其中,null 有属于自己的数据类型Null,引用类型中的数组、日期、正则 也都有属于自己的具体类型,而typeof对于这些类型的处理,只返回了处于其原型链最顶端的Object类型,没有错,但不是我们想要的结果。
2. instanceof
instanceof是用来判断A是否为B的实例,表达式为:A instanceof B
,如果A是B的实例,则返回true,否则返回false。在这里需要特别注意的是:instanceof
检测的是原型,我们用一段伪代码模拟其内部执行过程:
instanceof (A, B) {
var L = A.__proto__;
var R = B.prototype;
if (L === R) {
// A的内部属性__proto__指向B的原型对象
return true;
}
return false;
}
从上述过程可以看出,当A的__proto__
指向B的prototype
时,就认为A就是B的实例,我们再来看几个例子:
[] instanceof Array; // true
{} instanceof Object; // true
new Date() instanceof Date; // true
function Person() {};
new Person() instanceof Person; // true
[] instanceof Object; // true
new Date() instanceof Object; // true
new Person instanceof Object; // true
我们发现,虽然instanceof
能够判断出[]是Array的实例,但它认为[]也是Object的实例,为什么呢?
我们来分析一下[]、Array、Object三者之间的关系:
从instanceof
能够判断出[].__proto__
指向Array.prototype
,而Array.prototype.__proto__
又指向Object.prototype
,最终Object.prototype.__proto__
指向了null
,标志着原型链的结束。因此,[]、Array、Object
就在内部形成了一条原型链
从原型链可以看出,
[]
的__proto__
直接指向Array.prototype
,间接指向Object.prototype
,所以按照instanceof
的判断规则,[]
就是Object
的实例。依此类推,类似的new Date()、new Person()
也会形成一条对应的原型链。因此,instanceof
只能用来判断两个对象是否属于实例关系,而不能判断一个对象实例具体属于哪种类型。
instanceof操作符的问题在于,它假定只有一个全局执行环境。如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的构造函数。如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[0].Array;
var arr = new xArray(1, 2, 3); // [1, 2, 3]
arr instanceof Array; // false
针对数组的这个问题,ES5提供了Array.isArray()方法。该方法用以确认某个对象本身是否为Array类型,而不区分该对象在那个环境中创建。
if (Array.isArray(value)) {
// 对数组执行某些操作
}
Array.isArray()本质上检测的是对象的[[Class]]值,[[Class]]是对象的一个内部属性,里面包含了对象的类型信息,其格式为[object Xxx],Xxx就是对应的具体类型。对于数组而言,[[Class]]的值就是[object Array]。
3. constructor
当一个函数F被定义时,JS引擎会为F添加prototype
原型,然后再在prototype
上添加一个constructor
属性,并让其指向F的引用。如下所示:
当执行
var f = new F()
时,F被当成;饿构造函数,f是F的实例对象,此时F原型上的constructor
传递到了f上,因此f.constructor == F
可以看出,F利用原型对象上的constructor
引用了自身,当F作为构造函数来创建对象时,原型上的constructor
就被遗传到了新创建的对象上,从原型链角度讲,构造函数F就是新对象的类型。这样做的意义是,让新对象在诞生以后,就具有可追溯的数据类型。
同样,JavaScript中的内存对象在内部构建时也是这样做的:
细节问题:
1.null和undefined是无效的对象,因此不会有constructor存在的,这两种类型的数据需要通过其他方式来判断。
- 函数的constructor是不稳定的,这个主要体现在自定义对象上,当开发者重写prototype后,原有的constructor引用会丢失,constructor会默认为Object
为什么变成了Object?
因为prototype
被重新赋值的是一个{},{}是new Object()
字面量,因此new Object()
会将Object
原型上的constructor
传递给{},也就是Object
本身。
因此,为了规范开发,再重写对象原型时一般都需要重新给constructor赋值,以保证对象实例的类型不被篡改。
4. toString
toString()
是Object
的原型方法,调用该方法,默认返回当前对象的[[Class]]
。这是一个内部属性,其格式为[[object Xxx]]
,其中Xxx就是对象的类型。
对于Object
对象,直接调用toString()
就能返回[object Object]
。而对于其他对象,则需要call / apply
才能返回正确的类型信息。
Object.prototype.toString.call('') ; // [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(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(newFunction()) ; // [object Function]
Object.prototype.toString.call(newDate()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(newRegExp()) ; // [object RegExp]
Object.prototype.toString.call(newError()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object Window] window 是全局对象 global 的引用
9.可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用
ECMAScript 类型转换
JavaScript 的怪癖 1:隐式类型转换
JavaScript中,{}+{}等于多少?
JavaScript:将所有值都转换成对象
为什么 ++[[]][+[]]+[+[]] = 10?
自动转换Boolean
例如if
语句或者其他需要Boolean
的地方
if (表达式) {}
运算符
在非 Number
类型进行数学运算符- * /
时,会先将Number
转换为Number
类型。+
运算符要考虑字符串的情况,在操作数中存在字符串时,优先转换成字符串,+
运算符其中一个操作数是字符串的话,会进行连接字符串的操作
1 + '2' // '12'
+
操作符的执行顺序是:
- 当一侧操作数为
String
类型,会优先将另一侧转换为字符串类型。 - 当一侧操作数为
Number
类型,另一侧为原始类型,则将原始类型转换为Number
类型。 - 当一侧操作数为
Number
类型,另一侧为引用类型,将引用类型和Number
类型转换成字符串后拼接。
对象
只有在JavaScript表达式或语句中需要用到数字或者字符串时,对象才会被隐式转换。
当需要将对象转换为数字时,需要三个步骤
3*{valueof: function () {return 5}} // 15
否则,调用toString()
方法。如果结果是原始值,则将其转换为一个数字
3*{toString: function() {return 5}} // 15
否则,抛出一个类型错误
3*{toString: function() {return {}}} // TypeError: Cannot convert object to primitive value
10.出现小数精度丢失的原因,JavaScript可以存储的最大数字、最大安全数字,JavaScript处理大数字的方法、避免精度丢失的方法
JavaScript数字精度丢失问题总结
What Every Computer Scientist Should Know About Floating-Point Arithmetic
JS 的整型你懂了吗?
最大数字: Math.pow(2, 53) - 1
最大最小安全值
console.log(Number.MAX_SAFE_INTEGER) // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER) // -9007199254740991
如何处理大整数
第三方库:bignum、bigint。