面试宝典-JS篇

173 阅读7分钟

JS的数据类型有哪些?

JS的数据类型可分为8种2大类。

基本数据类型:number、string、boolean、null、undefined、symbol、bigint

引用数据类型:object(包含array、function、data、RegExp等)

null与undefined的区别

null和undefined基本是同义的,只有一些细微的差别。

  • null表示“没有对象”,即该处不应该有值。典型用法是:

(1)作为函数的参数,表示该函数的参数不是对象

(2)作为对象原型链的终点

Object.getPrototypeOf(Object.prototype)   //null
  • undefined表示“缺少值”,就是此处应该有一个值,但是还没有定义。典型用法是:

(1)变量被声明了,但没有赋值时,就等于undefined

(2)调用函数时,应该提供的参数没有提供,该参数等于undefined

(3)对象没有赋值的属性,该属性的值为undefined

(4)函数没有返回返回值时,默认返回undefined

var i; 
i  //undefined

var o = new Object();
o.p  //undefined

如何判断数据类型

判断方式有:typeof、instanceof、Object.prototype.toString.call()、constructor

  • typeof

适用于基本类型和函数判断,不适合用于Object类型的进一步判断。

const obj = { name: 'zhangsan', age: 18 } 
console.log(typeof obj)             //object  

const array = [1,2,3] 
console.log(typeof array)           //object 

const string = 'this is a string' 
console.log(typeof string)          //string

const boolean = true 
console.log(typeof boolean)         //boolean

const number = 1 
console.log(typeof number)          //number

const test_null = null 
console.log(typeof test_null)       //object

const test_undefined = undefined
console.log(typeof test_undefined)  //undefined

const func = () => {console.log(2)} 
console.log(typeof func)            //function
  • instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。 instanceof 不适用于判断基本类型。

// 定义构造函数
function C(){} 
function D(){} 

var o = new C();

o instanceof C;       // true,因为 Object.getPrototypeOf(o) === C.prototype
o instanceof D;       // false,因为 D.prototype 不在 o 的原型链上
o instanceof Object;  // true,因为 Object.prototype.isPrototypeOf(o) 返回 true

C.prototype instanceof Object    // true,同上
  • Object.prototype.toString.call()

toString() 方法返回一个表示该对象的字符串。默认情况下,toString() 方法被每个 Object 对象继承。

为了每个对象都能通过 Object.prototype.toString() 来检测,需要以 Function.prototype.call() 或者 Function.prototype.apply() 的形式来调用,传递要检查的对象作为第一个参数,称为 thisArg

var toString = Object.prototype.toString;

toString.call(1)                               //"[object Number]"
toString.call(true)                            //"[object Boolean]"
toString.call('string')                        //"[object String]"
toString.call({ name:'zhangsan', age: 18 })    //"[object Object]"
toString.call([1,2,3])                         //"[object Array]"
toString.call(undefined)                       //"[object Undefined]"
toString.call(null)                            //"[object Null]"
toString.call(new Date);                       //"[object Date]"
  • constructor

constructor是原型prototype的一个属性,当函数被定义时候,js引擎会为函数添加原型prototype,并且这个prototypeconstructor属性指向函数引用, 因此重写prototype会丢失原来的constructor。对于原始数据类型来说,constructor属性是只读的。

注:null和undefined没有构造函数,所以当检测 null 或者 undefined 类型的 constructor 属性时,js会报错!

const obj = {name: 'c',age: 21}
const arr = [1,2,3]
const str = 'cc'
const boolean = true
const number = 1
const func = () => {console.log(1)}
const temp_undefined = undefined
const temp_null = null

console.log(obj.constructor == Object)        // true
console.log(arr.constructor == Array)         // true
console.log(str.constructor == String)        // true
console.log(boolean.constructor == Boolean)   // true
console.log(number.constructor == Number)     // true
console.log(func.constructor == Function)     // true
console.log(temp_undefined.constructor)       // 报错
console.log(temp_null.constructor)            // 报错

原型与原型链

参考文章:JavaScript深入之从原型到原型链

什么是原型?

每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型“继承”属性。

每个函数都有一个prototype属性,称为显式原型。

每个实例对象都有一个__proto__属性,称为隐式原型。

prototype 是函数才会有的属性, 而__proto__ 是几乎所有对象都有的属性。

例子:

function Person() {

}
// prototype是函数才会有的属性
Person.prototype.name = 'Kevin';
let person1 = new Person();
let person2 = new Person();
console.log(person1.name);  //Kevin
console.log(person2.name);  //Kevin

prototype

每个函数都有一个prototype属性,就是我们例子中看到的prototype

那这个函数的prototype属性到底指向的是什么呢?是这个函数的原型吗?

其实,函数的prototype属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是例子中的person1person2的原型。

proto

这是每一个JavaScript对象(除了null)都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。

function Person() {

}
let person = new Person();
console.log(person.__proto__ === Person.prototype);  // true

既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?

constructor

指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数倒是有的,这就要讲到第三个属性:constructor,每个原型都有一个 constructor 属性指向关联的构造函数。

function Person() {

}
console.log(Person === Person.prototype.constructor);  // true

实例与原型

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。 举个例子:

function Person() {

}

Person.prototype.name = 'Kevin';

var person = new Person();

person.name = 'Daisy';
console.log(person.name) // Daisy

delete person.name;
console.log(person.name) // Kevin

在这个例子中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 Daisy。  

但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.__proto__ ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 Kevin。 

但是万一还没有找到呢?原型的原型又是什么呢?

原型的原型

在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是:

let obj = new Object();
obj.name = 'Kevin';
console.log(obj.name); //Kevin

其实原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 __proto__ 指向构造函数的 prototype

原型链

Object.prototype的原型呢?答案是null

Object.prototype.__proto__ 的值为 nullObject.prototype 没有原型,其实表达了一个意思。

 所以查找属性的时候查到 Object.prototype 就可以停止查找了。

由相互关联的原型组成的链状结构就是原型链,原型链通过__proto__链接起来。

继承有哪几种方式

  • 原型链继承
  • 借用构造函数(经典继承)
  • 组合继承(原型链继承和经典继承组合)
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承

作用域

  • 全局作用域
  • 函数作用域
  • 块级作用域

var、let、const的区别

  • var声明的变量会挂载在window上,存在变量提升
  • letconst声明形成块作用域,不存在变量提升。 同一个作用域下letconst不能声明同名变量,而var可以
  • const一旦声明必须赋值,不能使用null占位;声明后不能再修改;如果声明的是引用类型数据,可以修改其属性

变量提升、暂时性死区

console.log(name);  // undefined
var name = 'Kevin'

console.log(age);  // Uncaught ReferenceError: age is not defined
let age = 18;

变量提升

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined

也就是例子中的变量name在代码开始运行时,name就已经存在了,但是没有值,输出undefined

暂时性死区

如果区块中存在letconst命令,这个区块对这些声明的变量,从一开始就形成了封闭作用域。在使用命令声明变量之前,该变量都是不可用的。在语法上,称为“暂时性死区”。

例子中的age,在声明它之前,age是不存在的,这时候如果用到它,就会抛出一个错误。

new操作符

  • 创建一个空的简单JavaScript对象(即{}); 
  • 链接该对象(即设置该对象的构造函数)到另一个对象 ;
  • 将步骤1新创建的对象作为this的上下文 ;
  • 如果该函数没有返回对象,则返回this。

EventLoop

  1. 指向全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等) 
  2. 全局Script代码执行完毕后,调用栈Stack会清空 
  3. 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1 
  4. 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到把微任务队列中的所有任务都执行完毕。注意:如果在执行微任务的过程中,又产生了微任务,那么会加入到队列的末尾,也会在这个周期被调用执行 
  5. 微任务队列中的所有任务都执行完毕,此时微任务队列对空队列,调用栈Stack也为空 
  6. 取出宏队列中位于队首的任务,放入Stack中执行 
  7. 执行完毕后,调用栈Stack为空 
  8. 一直重复3-7步骤 

 这就是浏览器的事件循环Event Loop