JS基础知识及常见面试题
基础知识
一.数据类型
面试官:说说 JavaScript 中的基本类型有哪些?以及各个数据类型是如何存储的?
javaScript 的数据类型包括原始类型和引用类型(对象类型)。
原始类型包括以下 7 个:
- String
- Number
- Boolean
- null
- undefined
- Symbol
- bigint
引用类型统称为 Object 类型,如果细分的话,分为以下 5 个:
- Object
- Array
- Date
- RegExp
- Function
1、数据类型的存储形式
栈(Stack)和堆(Heap),是两种基本的数据结构。Stack 在内存中自动分配内存空间的;Heap 在内存中动态分配内存空间的,不一定会自动释放。一般我们在项目中将对象类型手动置为 null 原因,减少无用内存消耗。
原始类型是按值形式存放在栈中的数据段,内存空间可以自由分配,同时可以按值直接访问。
var a = 10;
var b = a;
b = 30;
console.log(a); // 10值
console.log(b); // 30值
过程图示:
引用类型是存放在堆内存中,每个对象在堆内存中有一个引用地址,就像是每个房间都有一个房间号一样。引用类型在栈中保存的就是这个对象在堆内存的引用地址,我们所说的“房间号”。通过“房间号”可以快速查找到保存在堆内存的对象。
var obj1 = new Object();
var obj2 = obj1;
obj2.name = "小鹿";
console.log(obj1.name); // 小鹿
过程图示:
2、typeof null === Object? null和undefined的区别
1.面试官:为什么 typeof null 等于 Object?
应用:一般我们在项目中将对象类型手动置为 null 原因,减少无用内存消耗。
不同的对象在底层原理的存储是用二进制表示的,在 javaScript中,如果二进制的前三位都为 0 的话,系统会判定为是 Object类型。null的存储二进制是 000,也是前三位,所以系统判定 null为 Object类型。
扩展:
这个 bug 个第一版的 javaScript留下来的。俺也进行扩展一下其他的几个类型标志位:
000:对象类型。
1:整型,数据是31位带符号整数。
010:双精度类型,数据是双精度数字。
100:字符串,数据是字符串。
110:布尔类型,数据是布尔值。
2.undefined 和 null 区别?
null和undefined的区别:
null 是一个表示"无"的对象,转为数值时为 0
undefined 是一个表示"无"的原始值,转为数值时为 NaN
当声明的变量还未被初始化时,变量的默认值为 undefined
null 用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象
undefined 表示 “缺少值”,就是此处应该有一个值,但是还没有定义。
典型用法是:
1. 变量被声明了,但没有赋值时,就等于 undefined
2. 调用函数时,应该提供的参数没有提供,该参数等于 undefined
3. 对象没有赋值的属性,该属性的值为 undefined
4. 函数没有返回值时,默认返回 undefined
null 表示“没有对象”,即该处不应该有值。
典型用法是:
1. 作为函数的参数,表示该函数的参数不是对象
2. 作为对象原型链的终点
undefined是访问一个未初始化的变量时返回的值,而null是访问一个尚未存在的对象时所返回的值。因此,可以把undefined看作是空的变量,而null看作是空的对象。
参考阅读
1.undefined 和 null 区别?
2.null 和 undefined 究竟有何区别?
3、数据类型的判断
面试官:typeof 与 instanceof 有什么区别?
typeof 是一元运算符,同样返回一个字符串类型。一般用来判断一个变量是否为空或者是什么类型。
除了 null 类型以及 Object 类型不能准确判断外,其他数据类型都可能返回正确的类型。
typeof undefined // 'undefined'
typeof '10' // 'String'
typeof 10 // 'Number'
typeof false // 'Boolean'
typeof Symbol() // 'Symbol'
typeof Function // ‘function'
typeof null // ‘Object’
typeof [] // 'Object'
typeof {} // 'Object'
既然 typeof 对对象类型都返回 Object 类型情况的局限性,我们可以使用 instanceof 来进行判断某个对象是不是另一个对象的实例。返回值的是一个布尔类型。
var a = [];
console.log(a instanceof Array) // true
instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性,如果对原型链不怎能了解,后边俺会具体的写到,这里大体记一下就 OK。
我们再测一下 ES6 中的 class 语法糖是什么类型。
class A{}
console.log(A instanceof Function) // true
注意:原型链中的prototype 随时可以被改动的,改变后的值可能不存在于 object的原型链上,instanceof返回的值可能就返回 false。
检测数组的六种方法
let arr = []
// 1. instanceof
arr instanceof Array
// 2. constructor
arr.constructor === Array
// 3. Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(arr)
// 4. getPrototypeOf
Object.getPrototypeOf(arr) === Array.prototype
// 5. Object.prototype.toString
Object.prototype.toString.call(arr) === '[object Array]'
// 6. Array.isArray ES6 新增
Array.isArray(arr)
1.Object.prototype.toString.call()
能准确的检测所有的数据类型
每一个继承Object的对象都有一个toString方法,如果toString方法没有被重写,会返回[Object type], type为对象的类型。
const arr = ["a", "b"]
arr.toString() // => "a, b"
Object.prototype.toString.call(arr) // => "[Object Array]"
2.Array.isArray()
用于判断对象是否为数组
Array.isArray()是ES5推出的,不支持ES6~8,使用时应考虑兼容
Array.isArray([1, 2, 3, 4]) // true
Array.isArray({a: 1}) // false
Array.isArray(new Array) // true
Array.isArray("string") // false
if(typeof Array.isArray != 'function') {
Array.isArray = function(obj){
return Object.prototype.toString.call(obj) == '[object Array]'
}
}
3.instanceof
能检测array、function、object类型
instanceof用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上,即 A 是否为 B 的实例:A instanceof B
1 instanceof Number // false
'str' instanceof String // false
true instanceof Boolean // false
[1,2,3] instanceof Array // true
function () {} instanceof Function // true
{} instanceof Object // true
instanceof 的缺点:是否处于原型链上的判断方法不严谨 instanceof 方法判断的是是否处于原型链上,而不是是不是处于原型链最后一位,所以会出现以下情况:
var arr = [1, 2, 3]
arr instanceof Array // true
arr instanceof Object // true
function fn() {}
fn instanceof Function // true
fn instanceof Object // true
所有原型链的尽头都是Object
instanceof, constructor,Object.prototype.isPrototypeOf,getPrototypeOf比较渣,丝毫不负责任,比如我们将 arr 的 proto 指向了 Array.prototype 后:
let arr = {
__proto__: Array.prototype
}
// 1. instanceof
arr instanceof Array // true
// 2. constructor
arr.constructor === Array // true
// 3. Object.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(arr) // true
// 4. getPrototypeOf
Object.getPrototypeOf(arr) === Array.prototype // true
js中!和!!的区别及用法
4、类型转换
类型转换通常在面试笔试中出现的比较多,对于类型转换的一些细节应聘者也是很容易忽略的,所以俺整理的尽量系统一些。javaScript是一种弱类型语言,变量不受类型限制,所以在特定情况下我们需要对类型进行转换。
【类型转换】分为显式类型转换和隐式类型转换。每种转换又分为原始类型转换和对象类型转换。
显式类型转换
显式类型转换就是我们所说强制类型转换。
笔试题:其他数据类型转字符串类型!
对于原始类型来说,转字符串类型会默认调用 toString() 方法。
数据类型 | String类型 |
---|---|
数字 | 转化为数字对应的字符串 |
true | 转化为字符串 “true” |
null | 转化为字符串 “null” |
undefined | 转化为字符串 “undefined” |
Object | 转化为 “[object Object]” |
String(123); // "123"
String(true); // "true"
String(null); // "null"
String(undefined);// "undefined"
String([1,2,3]) // "1,2,3"
String({}); // "[object Object]"
笔试题:其他数据类型转布尔类型!
除了特殊的几个值 ‘’、 undefined、 NAN、 null、 false、 0 转化为 Boolean 为 false 之外,其他类型值都转化为 true。
Boolean('') // false
Boolean(undefined) // false
Boolean(null) // false
Boolean(NaN) // false
Boolean(false) // false
Boolean(0) // false
Boolean({}) // true
Boolean([]) // true
笔试题:转化为数字类型!
数据类型 | 数字类型 |
---|---|
字符串 | ①数字转化为对应的数字 ②其他转化为 NaN |
布尔类型 | ①true 转化为 1 ②false 转化为 0 |
null | 0 |
undefined | NaN |
数组 | ①数组为空转化为 0;② 数组只有一个元素转化为对应元素;③其他转化为NaN |
空字符串 | 0 |
Number(10); // 10
Number('10'); // 10
Number(null); // 0
Number(''); // 0
Number(true); // 1
Number(false); // 0
Number([]); // 0
Number([1,2]); // NaN
Number('10a'); // NaN
笔试题:对象类型转原始类型!
对象类型在转原始类型的时候,会调用内置的 valueOf()和 toString() 方法,这两个方法是可以进行重写的。
转化原始类型分为两种情况:转化为字符串类型或其他原始类型。
- 如果已经是原始类型,不需要再进行转化。
- 如果转字符串类型,就调用内置函数中的 toString()方法。
- 如果是其他基本类型,则调用内置函数中的 valueOf()方法。
- 如果返回的不是原始类型,则会继续调用 toString() 方法。
- 如果还没有返回原始类型,则报错。
5、四则运算
隐士类型转化是不需要认为的强制类型转化,javaScript 自动将类型转化为需要的类型,所以称之为隐式类型转换。
加法运算
加法运算符是在运行时决定,到底是执行相加,还是执行连接。运算数的不同,导致了不同的语法行为,这种现象称为“重载”。
如果双方都不是字符串,则将转化为数字或字符串。
- Boolean + Boolean会转化为数字相加。
- Boolean + Number 布尔类型转化为数字相加。
- Object + Number 对象类型调用 valueOf,如果不是 String、Boolean或者 Number类型,则继续调用
toString()转化为字符串。
true + true // 2
1 + true // 2
[1] + 3 // '13'
- 字符串和字符串以及字符串和非字符串相加都会进行连接。
1 + 'b' // ‘1b’
false + 'b' // ‘falseb’
其他运算
其他算术运算符(比如减法、除法和乘法)都不会发生重载。它们的规则是:所有运算子一律转为数值,再进行相应的数学运算。
1 * '2' // 2
1 * [] // 0
6、逻辑运算符
逻辑运算符包括两种情况,分别为条件判断和赋值操作。
条件判断
- && :所有条件为真,整体才为真。
- || :只有一个条件为真,整体就为真。
true && true // true
true && false // false
true || true // true
true || false // true
赋值操作
- A && B
首先看 A 的真假, A 为假,返回 A 的值, A 为真返回 B 的值。(不管 B 是啥)
console.log(0 && 1) // 0
console.log(1 && 2) // 2
- A || B
首先看 A 的真假, A 为真返回的是 A 的值, A 为假返回的是 B 的值(不管 B 是啥)
console.log(0 || 1) // 1
console.log(1 || 2) // 1
7、比较运算符
比较运算符在逻辑语句中使用,以判定变量或值是否相等。
面试官:== 和 === 的区别?
对于 === 来说,是严格意义上的相等,会比较两个操作符的类型和值。
- 如果 X 和 Y 的类型不同,返回 false ;
- 如果 X 和 Y 的类型相同,则根据下方表格进一步判断
条件 | 例子 | 返回值 |
---|---|---|
undefined === undefined | undefined === undefined | true |
null === null | null === null | true |
String === String (当字符串顺序和字符完全相等的时候返回 true,否则返回 false) | ‘a’ === ‘a’ ‘a’ === ‘aa’ | true false |
Boolean === Boolean | true === true true === false | true false |
Symbol === Symbol | 相同的 Symbol 返回 true, 不相同的 Symbol 返回 false | |
Number === Number ① 其中一个为 NaN,返回 false ② X 和 Y 值相等,返回 true ③ 0 和 -0,返回 true ④ 其他返回 false | NaN ==== NaN NaN === 1 3 === 3 +0 === -0 | false false true true |
而对于 ==来说,是非严格意义上的相等,先判断两个操作符的类型是否相等,如果类型不同,则先进行类型转换,然后再判断值是否相等。
- 如果 X 和 Y 的类型相同,返回 X == Y 的比较结果;
- 如果 X 和 Y 的类型不同,根据下方表格进一步判断;
条件 | 例子 | 返回值 |
---|---|---|
null == undefined | null == undefined | true |
String == Number,String 转 Number | ‘2’ == 2 | true |
Boolean == Number,Boolean 转 Number | true == 1 | true |
Object == String,Number,Symbol,将 Object 转化为原始类型再比较值大小 | [1] == 1 [1] == ‘1’ | true true |
其他返回 false | false |
this
面试官:什么是 this 指针?以及各种情况下的 this 指向问题。
this就是一个对象。不同情况下 this指向的不同,有以下几种情况,(希望各位亲自测试一下,这样会更容易弄懂):
- 对象调用,this 指向该对象(前边谁调用 this 就指向谁)。
var obj = {
name:'小鹿',
age: '21',
print: function(){
console.log(this)
console.log(this.name + ':' + this.age)
}
}
// 通过对象的方式调用函数
obj.print(); // this 指向 obj
- 直接调用的函数,this指向的是全局 window对象。
function print(){
console.log(this);
}
// 全局调用函数
print(); // this 指向 window
- 通过 new的方式,this永远指向新创建的对象。
function Person(name, age){
this.name = name;
this.age = age;
console.log(this);
}
var xiaolu = new Person('小鹿',22); // this = > xaiolu
- 箭头函数中的 this。
由于箭头函数没有单独的 this值。箭头函数的 this与声明所在的上下文相同。也就是说调用箭头函数的时候,不会隐士的调用 this参数,而是从定义时的函数继承上下文。
const obj = {
a:()=>{
console.log(this);
}
}
// 对象调用箭头函数
obj.a(); // window
面试官:如何改变 this 的指向?
我们可以通过调用函数的 call、apply、bind 来改变 this的指向。
var obj = {
name:'小鹿',
age:'22',
adress:'小鹿动画学编程'
}
function print(){
console.log(this); // 打印 this 的指向
console.log(arguments); // 打印传递的参数
}
// 通过 call 改变 this 指向
print.call(obj,1,2,3);
// 通过 apply 改变 this 指向
print.apply(obj,[1,2,3]);
// 通过 bind 改变 this 的指向
let fn = print.bind(obj,1,2,3);
fn();
对于基本的使用想必各位小伙伴都能掌握,俺就不多废话,再说一说这三者的共同点和不同点。
共同点:
- 三者都能改变 this指向,且第一个传递的参数都是 this指向的对象。
- 三者都采用的后续传参的形式
不同点:
- call 的传参是单个传递的(试了下数组,也是可以的),而 apply 后续传递的参数是数组形式(传单个值会报错),而 bind没有规定,传递值和数组都可以。
- call 和 apply 函数的执行是直接执行的,而 bind 函数会返回一个函数,然后我们想要调用的时候才会执行。
扩展:如果我们使用上边的方法改变箭头函数的 this 指针,会发生什么情况呢?能否进行改变呢?
由于箭头函数没有自己的 this 指针,通过 call() 或 apply() 方法调用一个函数时,只能传递参数(不能绑定 this),他们的第一个参数会被忽略。
new
对于new
关键字,我们第一想到的就是在面向对象中 new
一个实例对象,但是在 JS 中的 new
和 Java 中的 new
的机制不一样。
一般 Java
中,声明一个构造函数
,通过new
类名() 来创建一个实例,而这个构造函数
是一种特殊的函数。但是在JS
中,只要 new
一个函数,就可以 new
一个对象,函数和构造函数没有任何的区别。
面试官:new 内部发生了什么过程?可不可以手写实现一个 new 操作符?
new 的过程包括以下四个阶段:
- 创建一个新对象。
- 这个新对象的 proto 属性指向原函数的 prototype 属性。(即继承原函数的原型)
- 将这个新对象绑定到此函数的 this 上 。
- 返回新对象,如果这个函数没有返回其他对象。
// new 生成对象的过程
// 1、生成新对象
// 2、链接到原型
// 3、绑定 this
// 4、返回新对象
// 参数:
// 1、Con: 接收一个构造函数
// 2、args:传入构造函数的参数
function create(Con, ...args){
// 创建空对象
let obj = {};
// 设置空对象的原型(链接对象的原型)
obj._proto_ = Con.prototype;
// 绑定 this 并执行构造函数(为对象设置属性)
let result = Con.apply(obj,args)
// 如果 result 没有其他选择的对象,就返回 obj 对象
return result instanceof Object ? result : obj;
}
// 构造函数
function Test(name, age) {
this.name = name
this.age = age
}
Test.prototype.sayName = function () {
console.log(this.name)
}
// 实现一个 new 操作符
const a = create(Test,'小鹿','23')
console.log(a.age)
面试官:有几种创建对象的方式,字面量相对于 new 创建对象有哪些优势?
最常用的创建对象的两种方式:
- new 构造函数
- 字面量
其他创建对象的方式:
Object.create()
字面量创建对象的优势所在:
- 代码量更少,更易读
- 对象字面量运行速度更快,它们可以在解析的时候被优化。他不会像 new一个对象一样,解析器需要顺着作用域链从当前作用域开始查找,如果在当前作用域找到了名为 Object()的函数就执行,如果没找到,就继续顺着作用域链往上照,直到找到全局 Object() 构造函数为止。
- Object()构造函数可以接收参数,通过这个参数可以把对象实例的创建过程委托给另一个内置构造函数,并返回另外一个对象实例,而这往往不是你想要的。
对于 Object.create()方式创建对象:
Object.create(proto, [propertiesObject]);
- proto:新创建对象的原型对象。
- propertiesObject:(可选)可为创建的新对象设置属性和值。
一般用于继承:
var People = function (name){
this.name = name;
};
People.prototype.sayName = function (){
console.log(this.name);
}
function Person(name, age){
this.age = age;
People.call(this, name); // 使用call,实现了People属性的继承
};
// 使用Object.create()方法,实现People原型方法的继承,并且修改了constructor指向
Person.prototype = Object.create(People.prototype, {
constructor: {
configurable: true,
enumerable: true,
value: Person,
writable: true
}
});
Person.prototype.sayAge = function (){
console.log(this.age);
}
var p1 = new Person('person1', 25);
p1.sayName(); //'person1'
p1.sayAge(); //25
面试官:new/字面量 与 Object.create(null) 创建对象的区别?
- new 和 字面量创建的对象的原型指向 Object.prototype,会继承 Object 的属性和方法。
- 而通过 Object.create(null) 创建的对象,其原型指向 null,null作为原型链的顶端,没有也不会继承任何属性和方法。
闭包
【推荐阅读】: JS高阶编程技巧(惰性函数、柯里化函数)
闭包面试中的重点,但是对于很多初学者来说都是懵懵的,所以俺就从最基础的作用域讲起,大佬请绕过。
面试官:什么是作用域?什么是作用域链?
规定变量和函数的可使用范围叫做作用域。只看定义,挺抽象的,举个例子:🌰
function fn1() {
let a = 1;
}
function fn2() {
let b = 2;
}
声明两个函数,分别创建量两个私有的作用域(可以理解为两个封闭容器),fn2 是不能直接访问私有作用域 fn1 的变量 a 的。同样的,在 fn1 中不能访问到 fn2 中的 b 变量的。一个函数就是一个作用域。
每个函数都会有一个作用域,查找变量或函数时,由局部作用域到全局作用域依次查找,这些作用域的集合就称为作用域链。 如果还不是很好理解,俺再举个例子:🌰
let a = 1
function fn() {
function fn1() {
function fn2() {
let c = 3;
console.log(a);
}
// 执行 fn2
fn2();
}
// 执行 fn1
fn1();
}
// 执行函数
fn();
虽然上边看起来嵌套有点复杂,我们前边说过,一个函数就是一个私有作用域,根据定义,在 fn2 作用域中打印 a,首先在自己所在作用域搜索,如果没有就向上级作用域搜索,直到搜索到全局作用域,a = 1,找到了打印出值。整个搜索的过程,就是基于作用域链搜索的。
面试官:什么是闭包?闭包的作用?闭包的应用?
很多应聘者喜欢这样回答,“函数里套一个函数”,但是面试官更喜欢下面的回答,因为可以继续为你挖坑。
函数执行,形成一个私有的作用域,保护里边的私有变量不受外界的干扰,除了保护私有变量外,还可以保存一些内容,这样的模式叫做闭包。
闭包的作用有两个,保护和保存。
保护的应用
-
团队开发时,每个开发者把自己的代码放在一个私有的作用域中,防止相互之间的变量命名冲突;把需要提供给别人的方法,通过 return 或
-
window.xxx 的方式暴露在全局下。
-
jQuery 的源码中也是利用了这种保护机制。 封装私有变量。
保存的应用
- 选项卡闭包的解决方案。
面试官:循环绑定事件引发的索引什么问题?怎么解决这种问题?
// 事件绑定引发的索引问题
var btnBox = document.getElementById('btnBox'),
inputs = btnBox.getElementsByTagName('input')
var len = inputs.length;
for(var i = 0; i < 1en; i++){
inputs[i].onclick = function () {
alert(i)
}
}
闭包剩余的部分,俺在之前的文章已经总结过,俺就不复制过来了,直接传送过去~ 动画:什么是闭包?
原型和原型链
面试官:什么是原型?什么是原型链?如何理解?
原型: 每个 JS 对象都有 proto 属性,这个属性指向了原型。跟俺去看看,
再来一个,
我们可以看到,只要是对象类型,都会有这个__proto__ 属性,这个属性指向的也是一个原型对象,原型对象也是对象呀,肯定也会存在一个 proto 属性。那么就形成了原型链,定义如下
原型链: 原型链就是多个对象通过 proto 的方式连接了起来形成一条链。
原型和原型链是怎么来的呢?如果理清原型链中的关系呢?
对于原型和原型链的前世今生,由于篇幅过大,俺的传送门~ 图解:告诉面试官什么是 JS 原型和原型链?
PS:下面的看不懂,一定去看文章哦!
构造函数与对象实例以及原型对象
构造函数的 prototype 指向原型对象,原型对象有一个 constructor 属性指回构造函数,每个构造函数生成的实例对象都有一个 proto 属性,这个属性指向原型对象。
再往深处看,他们之间存在复杂的关系,但是这些所谓的负责关系俺已经总结好了,小二上菜
这张图看起来真复杂,但是通过下边总结的,再来分析这张图,试试看。
- 所有的实例的 __proto__都指向该构造函数的原型对象(prototype)。
- 所有的函数(包括构造函数)是 Function() 的实例,所以所有函数的 __proto__的都指向 Function() 的原型对象。
- 所有的原型对象(包括 Function 的原型对象)都是 Object 的实例,所以 __proto__都指向 Object
(构造函数)的原型对象。而 Object 构造函数的 __proto__指向 null。 - Function 构造函数本身就是 Function 的实例,所以 __ proto__ 指向 Function 的原型对象。
面试官:instanceOf 的原理是什么?
之前留了一个小问题,总结了上述的原型和原型链之后,instanceof的原理很容易理解。
instanceof 的原理是通过判断该对象的原型链上是否可以找到该构造函数的 prototype 属性。 即 A 是否为 B 的实例:A instanceof B
1 instanceof Number // false
'str' instanceof String // false
true instanceof Boolean // false
[1,2,3] instanceof Array // true
function () {} instanceof Function // true
{} instanceof Object // true
function Foo(){}
var f1 = new Foo();
console.log(f1 instanceof Foo);// true
instanceof 的缺点:是否处于原型链上的判断方法不严谨 instanceof 方法判断的是是否处于原型链上,而不是是不是处于原型链最后一位,所以会出现以下情况:
var arr = [1, 2, 3]
arr instanceof Array // true
arr instanceof Object // true
function fn() {}
fn instanceof Function // true
fn instanceof Object // true
所有原型链的尽头都是Object
继承
推荐阅读:了解继承吗?该如何实现继承?
原型继承
function object(o){
function F(){}
F.prototype = o;
// 每次返回的 new 是不同的
return new F();
}
var person = {
friends : ["Van","Louis","Nick"]
};
// 实例 1
var anotherPerson = object(person);
anotherPerson.friends.push("Rob");
// 实例 2
var yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("Style");
// 都添加至原型对象的属性(所共享)
alert(person.friends); // "Van,Louis,Nick,Rob,Style"
① 基本思想: 创建临时性的构造函数(无任何属性),将传入的对象作为该构造函数的原型对象,然后返回这个新构造函数的实例。
② 浅拷贝:
object 所产生的对象是不相同的,但是原型对象都是 person 对象,所改变存在原型对象的属性所有生成的实例所共享,不仅被 Person 所拥有,而且被子类生成的实例所共享。
③ object.create(): 在 ECMAScript5 中,通过新增 object.create() 方法规范化了上面的原型式继承.。
参数一:新对象的原型的对象。
参数二:先对象定义额外的属性(可选)。
经典继承(构造函数)
/ 详细解析
//1、当用调用 call 方法时,this 带边 son 。
//2、此时 Father 构造函数中的 this 指向 son。
//3、也就是说 son 有了 colors 的属性。
//4、每 new 一个 son ,都会产生不同的对象,每个对象的属性都是相互独立的。
function Father(){
this.colors = ["red","blue","green"];
}
function Son(){
// this 是通过 new 操作内部的新对象 {} ,
// 此时 Father 中的 this 就是为 Son 中的新对象{}
// 新对象就有了新的属性,并返回得到 new 的新对象实例
// 继承了Father,且向父类型传递参数
Father.call(this);
}
let s = new Son();
console.log(s.color)
① 基本思想: 在子类的构造函数的内部调用父类的构造函数。
② 优点:
保证了原型链中引用类型的独立,不被所有实例共享。
子类创建的时候可以向父类进行传参。
③ 缺点:
继承的方法都在构造函数中定义,构造函数不能够复用了(因为构造函数中存在子类的特殊属性,所以构造函数中复用的属性不能复用了)。
父类中定义的方法对于子类型而言是不可见的(子类所有的属性都定义在父类的构造函数当中)。
组合继承
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
// 方法定义在原型对象上(共享)
Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name,age){
// 子类继承父类的属性
Father.call(this,name); //继承实例属性,第一次调用 Father()
// 每个实例都有自己的属性
this.age = age;
}
// 子类和父类共享的方法(实现了父类属性和方法的复用)
Son.prototype = new Father(); //继承父类方法,第二次调用 Father()
// 子类实例对象共享的方法
Son.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5
var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10
① 基本思想:
- 使用原型链实现对 【原型对象属性和方法】的继承。
- 通过借用构造函数来实现对【实例属性】的继承。
② 优点:
- 在原型对象上定义的方法实现了函数的复用。
- 每个实例都有属于自己的属性。
③ 缺点:
组合继承调用了两次父类的构造函数,造成了不必要的消耗。
寄生式继承
function createAnother(original){
var clone = object(original); // 通过调用object函数创建一个新对象
clone.sayHi = function(){ // 以某种方式来增强这个对象
alert("hi");
};
return clone; //返回这个对象
}
- 基本思想:不必为了指定子类型的原型而调用超类型的构造函数(避免第二次调用的构造函数)。
- 优点:寄生组合式继承就是为了解决组合继承中两次调用构造函数的开销。
ES6 class类的定义类的继承
在ES6之前,我们通过function来定义类,但是这种模式一直被很多从其他编程语言(比如Java、C++、OC等等)转到JavaScript的人所不适应。
原因是,大多数面向对象的语言,都是使用class关键字来定义类的。
而JavaScript也从ES6开始引入了class关键字,用于定义一个类。
ES6之前定义一个Person类:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.running = function() {
console.log(this.name + this.age + "running");
}
var p = new Person("why", 18);
p.running();
转换成ES6中的类如何定义呢?
类中有一个constructor构造方法,当我们通过new关键字调用时,就会默认执行这个构造方法
构造方法中可以给当前对象添加属性
类中也可以定义其他方法,这些方法会被放到Person类的prototype上
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {
console.log(this.name + this.age + "running");
}
}
const p = new Person("why", 18);
p.running();
另外,属性也可以直接定义在类中:
height和address是直接定义在类中
class Person {
height = 1.88;
address = "北京市";
constructor(name, age) {
this.name = name;
this.age = age;
}
studying() {
console.log(this.name + this.age + "studying");
}
}
继承是面向对象的一大特性,可以减少我们重复代码的编写,方便公共内容的抽取(也是很多面向对象语言中,多态的前提)。
ES6中增加了extends关键字来作为类的继承。
我们先写两个类没有继承的情况下,它们存在的重复代码:
Person类和Student类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {
console.log(this.name, this.age, "running");
}
}
class Student {
constructor(name, age, sno, score) {
this.name = name;
this.age = age;
this.sno = sno;
this.score = score;
}
running() {
console.log(this.name, this.age, "running");
}
studying() {
console.log(this.name, this.age, this.sno, this.score, "studing");
}
}
我们可以使用继承来简化代码:
注意:在constructor中,子类必须通过super来调用父类的构造方法,对父类进行初始化,否则会报错。
class Student1 extends Person {
constructor(name, age, sno, score) {
super(name, age);
this.sno = sno;
this.score = score;
}
studying() {
console.log(this.name, this.age, this.sno, this.score, "studing");
}
}
const stu1 = new Student1("why", 18, 110, 100);
stu1.studying();
垃圾回收机制
说到 Javascript的垃圾回收机制,我们要从内存泄漏一步步说起。
面试官:什么是内存泄漏?为什么会导致内存泄漏?
不再用到的内存,没有及时释放,就叫做内存泄漏。
内存泄漏是指我们已经无法再通过js代码来引用到某个对象,但垃圾回收器却认为这个对象还在被引用,因此在回收的时候不会释放它。 导致了分配的这块内存永远也无法被释放出来。如果这样的情况越来越多,会导致内存不够用而系统崩溃。
面试官:怎么解决内存泄漏?说一说 JS 垃圾回收机制的运行机制的原理?
【推荐阅读】
很多编程语言需要手动释放内存,但是很多开发者喜欢系统提供自动内存管理,减轻程序员的负担,这被称为"垃圾回收机制"。
之所以会有垃圾回收机制,是因为 js 中的字符串、对象、数组等只有确定固定大小时,才会动态分配内存,只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript 的解释器将会消耗完系统中所有可用的内存,造成系统崩溃
JavaScript与其他语言不同,它具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存。
两种垃圾回收策略
推荐阅读:
找出那些不再继续使用的变量,然后释放其内存。垃圾回收器会按照固定的时间间隔,周期性的执行该垃圾回收操作。
共有两种策略:
- 标记清除法
- 引用计数法
标记清除法
垃圾回收器会在运行的时候,会给存储在内存中的所有变量都加上标记,然后它会去掉环境中变量以及被环境中的变量引用的变量的标记。剩下的就视为即将要删除的变量,原因是在环境中无法访问到这些变量了。最后垃圾回收器完成内存清除操作。
它的实现原理就是通过判断一个变量是否在执行环境中被引用,来进行标记删除。
引用计数法
引用计数的垃圾收集策略不常用,引用计数的最基本含义就是跟踪记录每个值被引用的次数。
当声明变量并将一个引用类型的值赋值给该变量时,则这个值的引用次数加 1,同一值被赋予另一个变量,该值的引用计数加 1 。当引用该值的变量被另一个值所取代,则引用计数减 1,当计数为 0 的时候,说明无法在访问这个值了,所有系统将会收回该值所占用的内存空间。
存在的缺陷:
两个对象的相互循环引用,在函数执行完成的时候,两个对象相互的引用计数并未归 0 ,而是依然占据内存,无法回收,当该函数执行多次时,内存占用就会变多,导致大量的内存得不到回收。
最常见的就是在 IE BOM 和 DOM 中,使用的对象并不是 js 对象,所以垃圾回收是基于计数策略的。但是在 IE9 已经将 BOM 和 DOM 真正的转化为了 js 对象,所以循环引用的问题得到解决。
如何管理内存
虽然说是 js 的内存都是自动管理的,但是对于 js 还是存在一些问题的,最主要的一个问题就是分配给 Web 浏览器的可用内存数量通常比分配给桌面应用程序的少。
为了能够让页面获得最好的性能,必须确保 js 变量占用最少的内存,最好的方式就是将不用的变量引用释放掉,也叫做解除引用。
- 对于局部变量来说,函数执行完成离开环境变量,变量将自动解除。
- 对于全局变量我们需要进行手动解除。(注意:解除引用并不意味被收回,而是将变量真正的脱离执行环境,下一次垃圾回收将其收回)
var a = 20; // 在堆内存中给数值变量分配空间
alert(a + 100); // 使用内存
var a = null; // 使用完毕之后,释放内存空间
补充:因为通过上边的垃圾回收机制的标记清除法的原理得知,只有与环境变量失去引用的变量才会被标记回收,所用上述例子通过将对象的引用设置为 null ,此变量也就失去了引用,等待被垃圾回收器回收。
深拷贝和浅拷贝
面试官:什么是深拷贝?什么是浅拷贝?
上边在 JavaScript 基本类型中我们说到,数据类型分为基本类型和引用类型。对基本类型的拷贝就是对值复制进行一次拷贝,而对于引用类型来说,拷贝的不是值,而是值的地址,最终两个变量的地址指向的是同一个值。还是以前的例子:
var a = 10;
var b = a;
b = 30;
console.log(a); // 10值
console.log(b); // 30值
var obj1 = new Object();
var obj2 = obj1;
obj2.name = "小鹿";
console.log(obj1.name); // 小鹿
要想将 obj1 和 obj2 的关系断开,也就是不让他指向同一个地址。根据不同层次的拷贝,分为深拷贝和浅拷贝。
浅拷贝: 只进行一层关系的拷贝。
深拷贝: 进行无限层次的拷贝。
面试官:浅拷贝和深拷贝分别如何实现的?有哪几种实现方式?
- 自己实现一个浅拷贝:
// 实现浅克隆
function shallowClone(o){
const obj = {};
for(let i in o){
obj[i] = o[i]
}
return obj;
}
- 扩展运算符实现:
let a = {c: 1}
let b = {...a}
a.c = 2
console.log(b.c) // 1
- Object.assign()实现
let a = {c: 1}
let b = Object.assign({}, a)
a.c = 2
console.log(b.c) // 1
对于深拷贝来说,在浅拷贝的基础上加上递归,我们改动上边自己实现的浅拷贝代码:
var a1 = {b: {c: {d: 1}};
function clone(source) {
var target = {};
for(var i in source) {
if (source.hasOwnProperty(i)) {
if (typeof source[i] === 'object') {
target[i] = clone(source[i]); // 递归
} else {
target[i] = source[i];
}
}
}
return target;
}
如果功底稍微扎实的小伙伴可以看出上边深拷贝存在的问题:
- 参数没有做检验;
- 判断对象不够严谨;
- 没有考虑到数组,以及 ES6 的 set, map, weakset, weakmap兼容性。
- 最严重的问题就是递归容易爆栈(递归层次很深的时候)。
- 循环引用问题提。
var a = {};
a.a = a;
clone(a); // 会造成一个死循环
两种解决循环引用问题的办法:
- 暴力破解
- 循环检测
还有一个最简单的实现深拷贝的方式,那就是利用 JSON.parse(JSON.stringify(object)),但是也存在一定的局限性。
function cloneJSON(source) {
return JSON.parse(JSON.stringify(source));
}
对于这种方法来说,内部的原理实现也是使用的递归,递归到一定深度,也会出现爆栈问题。但是对于循环引用的问题不会出现,内部的解决方案正是用到了循环检测。对于详细的实现一个深拷贝,具体参考文章:深拷贝的终极探索
异步编程
由于 JavaScript 是单线程的,单线程就意味着阻塞问题,当一个任务执行完成之后才能执行下一个任务。这样就会导致出现页面卡死的状态,页面无响应,影响用户的体验,所以不得不出现了同步和异步的解决方案。
面试官:JS 为什么是单线程?又带来了哪些问题呢?
JS 单线程的特点就是同一时刻只能执行一个任。这是由一些与用户的互动以及操作 DOM 等相关的操作决定了 JS 要使用单线程,否则使用多线程会带来复杂的同步问题。如果执行同步问题的话,多线程需要加锁,执行任务造成非常的繁琐。
虽然 HTML5 标准规定,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。
上述开头我们也说到了,单线程带来的问题就是会导致阻塞问题,为了解决这个问题,就不得不涉及 JS 的两种任务,分别为同步任务和异步任务。
面试官:JS 如何实现异步编程?
最早的解决方案是使用回调函数,回调函数不是直接调用,而是在特定的事件或条件发生时另一方调用的,用于对该事件或条件进行响应。比如 Ajax 回调:
// jQuery 中的 ajax
$.ajax({
type : "post",
url : 'test.json',
dataType : 'json',
success : function(res) {
// 响应成功回调
},
fail: function(err){
// 响应失败回调
}
});
但是如果某个请求存在依赖性,如下:
$.ajax({
type:"post",
success: function(res){//成功回调
//再次异步请求
$.ajax({
type:"post",
url:"...?id=res.id,
success:function(res){
$.ajax({
type:"post",
url:"...?id=res.id,
success:function(){
// 往复循环
}
})
}
})
}
})
就会形成不断的循环嵌套,我们称之为回调地狱。我们可以看出回调地狱有以下缺点:
- 嵌套函数存在耦合性,一旦有所改动,牵一发而动全身。
- 嵌套函数一多,就很难处理错误。
- 回调函数不能使用 try catch 捕获异常(异常的捕获只能在函数
- 执行的时候才能捕获到)。
- 回调函数不能直接 return。
以上有两个地方俺需要再进一步详细说明一下:
- 为什么不能捕获异常?
其实这跟 js 的运行机制相关,异步任务执行完成会加入任务队列,当执行栈中没有可执行任务了,主线程取出任务队列中的异步任务并入栈执行,当异步任务执行的时候,捕获异常的函数已经在执行栈内退出了,所以异常无法被捕获。
- 为什么不能return?
return 只能终止回调的函数的执行,而不能终止外部代码的执行。
面试官:如何解决回调地狱问题呢?
既然回调函数存在回调地狱问题,那我们如何解决呢?ES6 给我们提供了三种解决方案,分别是 Generator、Promise、async/await(ES7)。
由于这部分涉及到 ES6 部分的知识,这一期是有关 JS 的,所以会在下一期进行延伸,这里不多涉及。
【留下一个传送门~】
面试官:说说异步代码的执行顺序?Event Loop 的运行机制是如何的运行的?
上边我们说到 JS 是单线程且使用同步和异步任务解决 JS 的阻塞问题,那么异步代码的执行顺序以及 EventLoop 是如何运作的呢?
在深入事件循环机制之前,需要弄懂一下几个概念:
- 执行上下文(Execution context)
- 执行栈(Execution stack)
- 微任务(micro-task)
- 宏任务(macro-task)
执行上下文
执行上下文是一个抽象的概念,可以理解为是代码执行的一个环境。JS 的执行上下文分为三种,全局执行上下文、函数(局部)执行上下文、Eval 执行上下文。
全局执行上下文: 执行上下文指的是全局 this 指向的 window,可以是外部加载的 JS 文件或者本地标签中的代码。
函数执行上下文: 函数上下文也称为局部上下文,每个函数被调用的时候,都会创建一个新的局部上下文。
Eval 执行上下文: 这个不经常用,所以不多讨论。
执行栈
执行栈,就是我们数据结构中的“栈”,它具有“先进后出”的特点,正是因为这种特点,在我们代码进行执行的时候,遇到一个执行上下文就将其依次压入执行栈中。
当代码执行的时候,先执行位于栈顶的执行上下文中的代码,当栈顶的执行上下文代码执行完毕就会出栈,继续执行下一个位于栈顶的执行上下文。
function foo() {
console.log('a');
bar();
console.log('b');
}
function bar() {
console.log('c');
}
foo();
- 初始化状态,执行栈任务为空。
- foo 函数执行,foo 进入执行栈,输出 a,碰到函数bar。
- 然后 bar 再进入执行栈,开始执行 bar 函数,输出 c。
- bar 函数执行完出栈,继续执行执行栈顶端的函数 foo,最后输出 c。
- foo 出栈,所有执行栈内任务执行完毕。
宏任务
对于宏任务一般包括:
- 整体的 script 标签内的代码,
- setTimeout
- setInterval
- setImmediate
I/O
微任务
对于微任务一般包括:
- Promise
- process.nextTick(Node)
- MutationObserver
注意:nextTick 队列会比 Promie 队列先执行。
以上概念弄明白之后,再来看循环机制是如何运行的呢?以下涉及到的任务执行顺序都是靠函数调用栈来实现的。
1)首先,事件循环机制的是从 <script>
标签内的代码开始的,上边我们提到过,整个 script 标签作为一个宏任务处理的。
2)在代码执行的过程中,如果遇到宏任务,如:setTimeout,就会将当前任务分发到对应的执行队列中去。
3)当执行过程中,如果遇到微任务,如:Pomise,在创建 Promise实例对象时,代码顺序执行,如果到了执行· then 操作,该任务就会被分发到微任务队列中去。
4)script 标签内的代码执行完毕,同时执行过程中所涉及到的宏任务也和微任务也分配到相应的队列中去。
5)此时宏任务执行完毕,然后去微任务队列执行所有的存在的微任务。
6)微任务执行完毕,第一轮的消息循环执行完毕,页面进行一次渲染。
7)然后开始第二轮的消息循环,从宏任务队列中取出任务执行。
8)如果两个任务队列没有任务可执行了,此时所有任务执行完毕。
实战一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>消息运行机制</title>
</head>
<body>
</body>
<script>
console.log('1');
setTimeout(() => {
console.log('2')
}, 1000);
new Promise((resolve, reject) => {
console.log('3');
resolve();
console.log('4');
}).then(() => {
console.log('5');
});
console.log('6');// 1,3,4,6,5,2
</script>
</html>
- 初始化状态,执行栈为空。
- 首先执行·
<script>
标签内的同步代码,此时全局的代码进入执行栈中,同步顺序执行代码,输出 1。 - 执行过程中遇到异步代码 setTimeout(宏任务),将其分配到宏任务异步队列中。
- 同步代码继续执行,遇到一个 promise 异步代码(微任务)。但是构造函数中的代码为同步代码,依次输出3、4,则 then之后的任务加入到微任务队列中去。
- 最后执行同步代码,输出 6。
- 因为script内的代码作为宏任务处理,所以此次循环进行到处理微任务队列中的所有异步任务,直达微任务队列中的所有任务执行完成为止,微任务队列中只有一个微任务,所以输出
5。 - 此时页面要进行一次页面渲染,渲染完成之后,进行下一次循环。
- 在宏任务队列中取出一个宏任务,也就是之前的 setTimeout,最后输出 2。
- 此时任务队列为空,执行栈中为空,整个程序执行完毕。
以上难免有些啰嗦,所以简化整理如下步骤: - 一开始执行宏任务(script中同步代码),执行完毕,调用栈为空。
- 然后检查微任务队列是否有可执行任务,执行完所有微任务。
- 进行页面渲染。
- 第二轮从宏任务队列取出一个宏任务执行,重复以上循环。
参考文献
1.https://www.cnblogs.com/xiaoheimiaoer/p/4572558.html
2.https://juejin.im/entry/584918612f301e005716add6
3.https://juejin.im/post/5ba32171f265da0ab719a6d7
4.https://segmentfault.com/a/1190000012646203
5.前端面试之道
6.https://segmentfault.com/a/1190000016672263
——————————————
版权声明:本文为CSDN博主「小鹿动画学编程」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_36903042/article/details/104207646
常考面试题
推荐阅读
原生JS灵魂之问, 请问你能接得住几个?(上)
JavaScript 数据类型检测终极解决方案
第一篇: JS数据类型之问——概念篇
1.面试官:JS原始数据类型有哪些?引用数据类型有哪些?区别是什么?
基本数据类型:
a. 基本数据类型的值是不可变的,这里你就可以联想到,是不是所有关于字符串和数字的方法都是带有返回值的,而不是改变原字符串或数字。
例如
let a='abc';
a.split('');
console.log(a);//abc
b. 基本数据类型不可以添加属性和方法,虽然不会报错,但也只是一瞬间转为了相应包装对象,操作完又转化回原基本数据类型,不会保存结果。
c. 基本数据类型的赋值是简单赋值,基本数据类型的比较是值的比较。
d. 基本数据类型是存放在栈区的
引用数据类型:
a. 引用类型的值是可以改变的,例如对象就可以通过修改对象属性值更改对象。
b. 引用类型可以添加属性和方法。
c. 引用类型的赋值是对象引用,即声明的变量标识符,存储的只是对象的指针地址。
d. 引用类型的比较是引用(指针地址)的比较。
e. 引用类型是同时保存在栈区和堆区中的,栈区保存变量标识符和指向堆内存的地址。
2.面试官:Bigint和Number的区别
Number类型的数字有精度限制,数值的精度只能到 53 个二进制位(相当于 16 个十进制位, 正负9007199254740992),大于这个范围的整数,就无法精确表示了。
Bigint没有位数的限制,任何位数的整数都可以精确表示。但是其只能用于表示整数,且为了与Number进行区分,BigInt 类型的数据必须添加后缀n。BigInt 可以使用负号(-),但是不能使用正号(+)。
另外number类型的数字和Bigint类型的数字不能混合计算。
12n+12;//报错
3.面试官:为什么 typeof null 等于 Object?
结论: null不是对象。
解释: 虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。
4.面试官: undefined 和 null 区别?
undefined是访问一个未初始化的变量时返回的值,而null是访问一个尚未存在的对象时所返回的值。因此,可以把undefined看作是空的变量,而null看作是空的对象。
5.面试官:‘1’.toString()为什么可以调用?
其实在这个语句运行的过程中做了这样几件事情:
var s = new Object('1');
s.toString();
s = null;
第一步: 创建Object类实例。注意为什么不是String ?由于Symbol和BigInt的出现,对它们调用new都会报错,目前ES6规范也不建议用new来创建基本类型的包装类。
第二步: 调用实例方法。
第三步: 执行完方法立即销毁这个实例。
整个过程体现了 基本包装类型的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean, Number和String。
参考:《JavaScript高级程序设计(第三版)》P118
6.面试官:5.0.1+0.2为什么不等于0.3?
0.1和0.2在转换成二进制后会无限循环,由于标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004。
7.面试官:js中的堆和栈,栈和队列有什么区别
堆(heap)和栈(stack)的区别:
堆:队列优先,先进先出;由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
栈:先进后出;动态分配的空间 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
栈和队列的区别:
a. 栈只允许在表尾一端进行插入和删除,队列只允许在表尾一端进行插入,在表头一端进行删除。
b. 栈是先进后出,队列是先进先出。