JavaScript经典面试题
- 1.简要描述 JS 的数据类型?
- 2. && || 和 !! 运算符分别能做什么
- 3.什么是 “逻辑短路” ?
- 4.简述 Null 和 Undefined 的区别
- 5.js的数据类型的转换
- 6.JS中数据类型的判断( typeof,instanceof,constructor,Object.prototype.toString.call()
- 7.介绍 js 有哪些内置对象?
- 8.undefined 与 undeclared 的区别?
- 9.{} 和 [] 的 valueOf 和 toString 的结果是什么?
- 10.Javascript 的作用域和作用域链
- 11.谈谈你对this、call、apply和bind的理解
- 12.javascript 创建对象的几种方式?
- 13. '1'.toString()为什么可以调用?
- 14. 0.1+0.2为什么不等于0.3 ?
- 15. instanceof能否判断基本数据类型?
- 16. [] == ![] 结果是什么?为什么?
- 17. 如何让 if (a == 1 && a == 2)条件成立?
- 18.模拟实现一个new的效果?
- 19.完整版的深拷贝
1.简要描述 JS 的数据类型?
JavaScript 的数据类型可分为
原始数据类型
和引用数据类型
。
原始数据类型:String
(字符串)、Number
(数字)、Boolean
(布尔)、Null
(空)、Undefined
(未定义)、Symbol
(es6新增,表示独一无二的值)、BigInt
(es10新增,比Number数据类型支持更大范围的整数值)
原始数据类型存储在栈(stack)
内存中,占据空间小、大小固定,属于被频繁使用的数据,所以放在栈中存储。
引用数据类型:Object
(Object本质上是由一组无序的名值对组成的),里面包含 function、Array、Date等等。
引用数据类型同时存储在栈(stack)
和堆(heap)
中,占据空间大、大小不固定。只是在栈
中存储了指针
,该指针指向堆
中该实体
的起始地址
,当解释器寻找引用值时,会先检索其在栈中的指针,取得后从堆中获取到实体
2. && || 和 !! 运算符分别能做什么
- &&:逻辑与,在其操作数中找到第一个虚值表达式并返回它,如果没有找到任何虚值表达式,则返回最后一个真值表达式。它采用短路来防止不必要的工作。
- ||:逻辑或,在其操作数中找到第一个真值表达式并返回它。这也使用了短路来防止不必要的工作。在支持ES6默认函数参数之前,它用于初始化函数中的默认参数值。
- !!:运算符可以将右侧的值强制转换为布尔值,这也是将值转换为布尔值的一种简单方法。
3.什么是 “逻辑短路” ?
逻辑短路是对于逻辑运算而言的,是指,仅计算逻辑表达式中的一部分便能确定结果,而不对整个表达式进行运算的现象。
对于&&
运算符,当第一个操作数为false
时,将不会判断第二个操作数,因为此时无论第二个操作数为何,最后的运算结果一定是false
对于||
运算符,当地一个操作数为true
时,将不会判断第二个操作数,因为此时无论第二个操作数为何,最后的运算结果一定是true
4.简述 Null 和 Undefined 的区别
Null
:是JS的关键字,用于描述空值
,对其执行 typeof 操作,返回object
,即为一个特殊的对象值,可以表示数字、字符串、对象是无值
的
Undefined
:是预定义的全局变量,其值为未定义
,它是变量的一种取值,表示变量没有初始化,当查询对象属性、数组元素的值时,如果返回 Undefined
则表示属性或者元素不存在,如果函数没有任何返回值,也返回undefined
注意:
虽然null 和 Undefined 是不同的,但是因为都表示值的空缺
,两者可以互换,因此,使用==
认为二者是相等的,需要用===
来区分
5.js的数据类型的转换
在js中类型转换只有三种情况:
- 转换为布尔值(调用
Boolean()
方法) - 转换为数字(调用
Number() 、parseInt()和parseFloat()方法
) - 转换为字符串(调用
.toString()或String()方法
)
注:null
和undefined
没有 .toString()
方法
原始值 | 转换值 | 结果 |
number string undefined、null 引用类型 | 布尔值 | number里除了+0 -0 NaN都是 false string里除了空字符串都为true null和undefined为false 引用类型为true |
number Boolean、function、Symbol 数组 对象 | 字符串 | number就是'number' true或false就是'true'或'false' 。 function就是'function(){}' 。Symbol就是'Symbol()' 数组 [1,2]就是'1,2' ;空数组转化就是空字符串 对象 就是 '[object Object]' |
string 数组 null 除了数组之外其他的引用类型 Symbol | 数字 | '1'就是1,'a'就是NaN 空数组[]为0,数组中有一个元素并且是数字 转为 该数字,其他情况都为NaN null为0 除数组之外的引用类型都为NaN Symbol报错 (Uncaught TypeError: Cannot convert a Symbol value to a number) |
此外还有一些操作符会存在隐式转换
6.JS中数据类型的判断( typeof,instanceof,constructor,Object.prototype.toString.call()
- typeof对于原始类型来说,除了 null 都可以显示正确的类型
- instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
- constructor用于返回创建该对象的函数
- Object.prototype.toString使用 Object 对象的原型方法 toString ,使用 call 进行狸猫换太子,借用Object的 toString 方法
封装一个判断数据类型的函数
,之前详细介绍过这里就不举例子多说了
7.介绍 js 有哪些内置对象?
全局的对象( global objects )或称标准内置对象
,不要和 "全局对象(global object)"
混淆。这里说的全局的对象是说在全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。
标准内置对象的分类
- 值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。
例如Infinity、NaN、undefined、null 字面量
- 函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。
例如eval()、parseFloat()、parseInt() 等
- 基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。
例如Object、Function、Boolean、Symbol、Error 等
- 数字和日期对象,用来表示数字、日期和执行数学计算的对象。
例如Number、Math、Date
- 字符串,用来表示和操作字符串的对象。
例如String、RegExp
- 可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如
Array
- 使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。
例如Map、Set、WeakMap、WeakSet
- 矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。
例如SIMD 等
- 结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。
例如JSON 等
- 控制抽象对象
例如Promise、Generator 等
- 反射
例如Reflect、Proxy
- 国际化,为了支持多语言处理而加入 ECMAScript 的对象。
例如Intl、Intl.Collator 等
- WebAssembly
- 其他
例如 arguments
js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般我们经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象
8.undefined 与 undeclared 的区别?
已在作用域中声明但还没有赋值
的变量,是 undefined
的。相反,还没有在作用域中声明
过的变量,是 undeclared
的。
对于 undeclared
变量的引用,浏览器会报引用错误,如 ReferenceError: b is not defined 。
但是我们可以使用 typeof
的安全防范机制来避免报错,因为对于 undeclared
(或者 not defined
)变量,typeof
会返回 "undefined"
。
9.{} 和 [] 的 valueOf 和 toString 的结果是什么?
var obj = {}
obj.valueOf() // {}
obj.toString() // "[object Object]"
var arr = []
arr.valueOf() // []
arr.toString() // ""
10.Javascript 的作用域和作用域链
作用域: 作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找。
变量的作用域无非就是两种:
全局变量
和局部变量
。
全局作用域
:
最外层函数定义的变量拥有全局作用域,即对任何内部函数来说,都是可以访问的
局部作用域
:
和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的,最常见的例如函数内部
作用域链: 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和
函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找。
执行环境
想要理解js怎么链式查找,就得先了解js的执行环境
每个函数运行时都会产生一个执行环境,而这个执行环境怎么表示呢?
js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。
js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。
11.谈谈你对this、call、apply和bind的理解
- 在浏览器里,在全局范围内this 指向window对象;
- 在函数中,this永远指向最后调用他的那个对象;
- 构造函数中,this指向new出来的那个新的对象;
- call、apply、bind中的this被强绑定在指定的那个对象上;
- 箭头函数中this比较特殊,箭头函数this为父作用域的this,不是调用时的this.要知道前四种方式,都是调用时确定,也就是动态的,而箭头函数的this指向是静态的,声明的时候就确定了下来;
- apply、call、bind都是js给函数内置的一些API,调用他们可以为函数指定this的执行,同时也可以传参。
12.javascript 创建对象的几种方式?
Object构造函数创建
var Person = new Object();
Person.name = "xiaobai";
Person.age = 26;
创建了Object引用类型的一个新实例,然后把实例保存在变量Person中。
使用对象字面量表示法
var Person = {};//相当于 var Person = new Object();
var Person = {
name:'xiaobai',
age:26
}
对象字面量是对象定义的一种简写形式。和Object构造函数创建对象其实都是一样的。
这两种写法的缺陷及优化:
-
缺陷:它们都是用了一个接口创建很多对象,会产生大量的重复代码。
-
优化:所以为了避免过多的重复代码,把创建对象的过程封装在函数体内,通过函数的调用直接生成对象
使用工厂模式创建对象
function Person(name,age,job){
var obj = new Object();
obj.name = name;
obj.age = age;
obj.job = job;
obj.sayHello = function(){
console.log("hello!!!")
};
return obj;
}
var person1 = Person('xiaobai',26,'Web');
var person2 = Person('laowang',35,'QA');
在使用工厂模式创建对象的时候,在Person函数中返回的是一个对象。我们无法判断返回的对象是一个什么样的类型。于是又出现了构造函数创建对象的模式。
使用构造函数创建对象
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayHello = function(){
console.log('hello!!!');
};
}
var person1 = new Person('xiaobai',26,'Web');
var person2 = new Person('laowang',35,'QA');
对比工厂模式,可以发现一下区别:
- 没有显示的创建对象
- 直接将属性和方法赋给了this对象
- 没有return语句
- 可以识别对象的类型
这里用instanceof来检测对象类型
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
构造函数确实很好用,但是它也有它的缺点:
就是每个方法都要在每个实例上重新创建一遍,方法指的就是我们在对象里面定义的函数。如果方法数量很多就会占用很多不必要的内存。
于是就出现了原型创建对象模式。
原型创建对象模式
function Person(){}
Person.prototype.name = 'xiaobai';
Person.prototype.age = 26;
Person.prototype.job = 'Web';
Person.prototype.sayHello = function(){
console.log('hello!!!')
};
var person1 = new Person();
var person2 = new Person();
person1.name = "laowang";
console.log(person1.name); // 'laowang' ---来自实例
console.log(person2.name); // 'xiaobai' ---来自原型
使用原型创建对象的方式,可以让所有对象实例共享它所包含的属性和方法。
当为对象实例添加一个属性时,这个属性就会屏蔽
原型对象中保存的同名属性。
这时候我们就可以使用构造函数模式与原型模式结合的方式,构造函数
模式用于定义实例属性
,而原型
模式定义方法和共享的属性
。
组合使用构造函数模式和原型模式
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype = {
constructor:Person,
sayName:function(){
console.log(this.name)
}
}
var person1 = new Person('xiaobai',26,'Web')
13. ‘1’.toString()为什么可以调用?
其实在这个语句运行的过程中做了这样几件事情:
var s = new Object('1');
s.toString();
s = null;
第一步:创建Object类实例。注意为什么不是String ? 由于Symbol和BigInt的出现,对它们调用new都会报错,目前ES6规范也不建议用new来创建基本类型的包装类。
第二步:调用实例方法。
第三步:执行完方法立即销毁这个实例。
整个过程体现了 基本包装类型
的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean、Number、String。
14. 0.1+0.2为什么不等于0.3 ?
0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成
0.30000000000000004。
15. instanceof能否判断基本数据类型?
当然阔以,比如下面这种方式,直接搞起代码:
class PromitiveNumber {
static [Symbol.hasInstance](x) {
return typeof x === 'number'
}
}
console.log(111 instanceof PrimitiveNumber) // true
其实就是自定义instanceof行为的一种方式,这里将原有的instanceof方法重定义,换成了typeof,因
此能够判断基本数据类型。
16. [] == ![] 结果是什么?为什么?
==中,左右两边都需要转化成数字然后比较
[]转化成数字是0
![]是先转化成布尔值,由于[]作为一个引用类型转换为布尔值为true,因此![]为false,进而在转换成数字,变为0。
所以0 == 0,结果为true
17. 如何让 if (a == 1 && a == 2)条件成立?
let a = {
value: 0,
valueOf: function() {
this.value++;
return this.value;
}
}
console.log(a == 1 && a == 2); // true
18.模拟实现一个new的效果?
new 被调用后做了三件事情:
1)让实例可以访问到私有属性
2)让实例可以访问构造函数原型(constructor.prototype)所在原型链上的属性
3)如果构造函数返回的结果不是引用数据类型
function newOperator(ctor, ...args) {
if(typeof ctor !== 'function'){
throw 'newOperator function the first param must be a function';
}
let obj = Object.create(ctor.prototype);
let res = ctor.apply(obj, args);
let isObject = typeof res === 'object' && res !== null;
let isFunction = typoof res === 'function';
return isObect || isFunction ? res : obj;
};
19.完整版的深拷贝
const getType = obj => Object.prototype.toString.call(obj);
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
const canTraverse = {
'[object Map]': true,
'[object Set]': true,
'[object Array]': true,
'[object Object]': true,
'[object Arguments]': true,
};
const mapTag = '[object Map]';
const setTag = '[object Set]';
const boolTag = '[object Boolean]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
const handleRegExp = (target) => {
const { source, flags } = target;
return new target.constructor(source, flags);
}
const handleFunc = (func) => {
// 箭头函数直接返回自身
if(!func.prototype) return func;
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
// 分别匹配 函数参数 和 函数体
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if(!body) return null;
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
}
const handleNotTraverse = (target, tag) => {
const Ctor = target.constructor;
switch(tag) {
case boolTag:
return new Object(Boolean.prototype.valueOf.call(target));
case numberTag:
return new Object(Number.prototype.valueOf.call(target));
case stringTag:
return new Object(String.prototype.valueOf.call(target));
case symbolTag:
return new Object(Symbol.prototype.valueOf.call(target));
case errorTag:
case dateTag:
return new Ctor(target);
case regexpTag:
return handleRegExp(target);
case funcTag:
return handleFunc(target);
default:
return new Ctor(target);
}
}
const deepClone = (target, map = new WeakMap()) => {
if(!isObject(target)) return target;
let type = getType(target);
let cloneTarget;
if(!canTraverse[type]) {
// 处理不能遍历的对象
return handleNotTraverse(target, type);
}else {
// 这波操作相当关键,可以保证对象的原型不丢失!
let ctor = target.constructor;
cloneTarget = new ctor();
}
if(map.get(target)) return target;
map.set(target, true);
if(type === mapTag) {
//处理Map
target.forEach((item, key) => {
cloneTarget.set(deepClone(key, map), deepClone(item, map));
})
}
if(type === setTag) {
//处理Set
target.forEach(item => {
cloneTarget.add(deepClone(item, map));
})
}
// 处理数组和对象
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop], map);
}
}
return cloneTarget;
}