前端面试JS基础
Contents
1 JS的数据类型有哪些
-
8种
- 数字(
number
)、字符串(string
)、布尔(boolean
)、空(null
)、未定义(undefined
)、对象(object
)、bigint
、symbol
- 数字(
-
null和undefined
- null一般用来表示空对象
- undefined一般用来表示一个非对象为空
-
为什么引入bigint
- JS的number默认是双精度浮点数,超过最大支持长度($$2^{53}-1$$)的整数将被四舍五入
- 为了能准确的表示最大精度以外的数,es6新增了Bigint类型
- Bigint === number => false 因为类型不同
- 只支持整数,不支持小数
2 原型链
2.1 什么是原型链(举例)
- JS中一切皆对象,只要是对象(包括简单类型的对象)就有一个隐藏属性
__proto__
(Object.prototype除外),指向(全等于)其构造函数的原型(prototype
);__proto__
属性的作用就是指向对象的原型的,告诉对象其构造函数的原型是谁 - 比如说有个数组a,那么a的隐藏属性
__proto__
指向a(构造函数)的原型,即Array.prototype
,而Array.prototype也有隐藏属性__proto__
,指向Object.prototype
,因为全部对象的祖先原型都是Object.prototype - 至此,数组a、Array.prototype与Object.prototype就通过隐藏属性
__proto__
形成了一条原型链,一般原型链的长度在2到3层
2.2 如何实现原型链
- 本质就是改变对象的隐藏属性
__proto__
,使其指向另外一个对象 - 但不推荐直接赋值更改
const x = Object.create(新原型对象)
const x = new 新原型对象的构造函数()
2.3 优点
- 在没有Class的情况下实现继承,以
[] => Array.prototype => Object.prototype
的原型链为例- a是Array的实例,a拥有Array.prototype的属性
- Array继承了Object
- 那么a就是Object的间接实例,也拥有Object.prototype的属性
2.4 缺点
- 不支持私有属性,只能靠
_
或__
作为前缀来约定私有属性- 在class中利用
#
作为前缀来定义私有属性
- 在class中利用
- 写起来较复杂
3 this
-
this永远指向最后调用它的那个对象
-
箭头函数没有this,即始终指向函数定义时的this
-
this就是call一个函数时,传入的第一个参数
-
-
例题:
|
|
- 答案
|
|
4 new做了什么
- 创建临时对象
const obj = {}
- 将临时对象的
__proto__
指向构造函数的原型(绑定原型,获取构造函数原型链上的共有属性)obj.__proto__ = Con.prototype
或者Object.setPrototypeOf(obj, Con.prototype)
- 执行构造函数,绑定构造函数的this为这个临时对象(同时赋值,获取自身属性)
const result = Con.apply(obj, ...args)
- 返回临时对象
return result
5 什么是立即执行函数
-
是什么:声明一个函数,然后立即执行的方法(或过程)
-
怎么做:
|
|
- 解决了什么问题:
在ES6之前,只能通过它来创建局部作用域
其他方法都需要借助全局变量。比如借助一个函数,在函数中虽然可以定义局部变量,但此时这个函数就是全局变量
- 优点:
兼容性好
- 缺点:
可读性差
- 怎么解决缺点:
使用ES6的block+let/const
语法
|
|
6 什么是闭包
-
是什么:
- 函数以及函数能访问到的变量(自由变量)的总和就是闭包
- 闭包是JS的一种语法特性(既不是对象也不是函数)
- 对于一个函数来说,变量分为全局变量、局部变量以及自由变量
-
怎么做:
|
|
-
解决了什么问题:
-
避免污染全局环境
-
可以通过提供对局部变量的间接访问接口,维持(保护)一个变量
-
|
|
-
优点:简单易用
-
缺点:闭包在使用不当的情况下会造成内存泄漏,不过其核心原因是浏览器(IE)的问题
-
如何解决缺点:不用,少用,慎用
7 JS如何实现类
- 使用原型
|
|
- 使用class
|
|
8 继承
-
原型与原型链
-
函数本身作为自身原型对象的构造函数
-
实例的
__proto__
属性(原型)等于其构造函数的prototype
属性 -
所有对象的祖先原型都是
Object.prototype
-
实例对象会改变原型对象的引用类型属性(而构造函数中的属性不会受到影响)
-
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// 构造函数 function Parent(){ // 构造函数的属性,实例化时会复制到实例对象上 this.age = 15 } // 原型对象的属性,实例化时不会直接复制到实例对象上 // 实例对象会改变原型对象中的引用类型属性 Parent.prototype.name = 'gsq' // 实例化Parent为对象p const p = new Parent() // 也可以直接通过原型对象上的构造函数进行实例化 const p1 = new Parent.prototype.constructor // p可以访问原型对象上的name属性 console.log(p.name) // 'gsq' // 实例对象上的同名属性会覆盖原型对象的属性 p.name = 'zs'
-
-
访问原型对象的方式
-
构造函数.prototype
-
实例对象.__proto__
(不推荐) -
Object.getPrototypeOf(实例对象)
(推荐)
-
-
继承
-
原型链继承:
-
子类的原型对象指向父类的实例对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
// 构造函数Parent function Parent () { // 构造函数中的实例属性 this.name = 'gsq' this.array = [1, 2, 3] } // 原型属性 Parent.prototype.array2 = [4, 5, 6] // 原型方法 Parent.prototype.say(){ console.log('say something') } // 构造函数Child function Child () {} // 原型链继承核心 Child.prototype = new Parent() // 可有可无的细节: // 重新建立子类实例与子类构造函数的联系 Object.defineProperty(Child.prototype, "constructor", { value: Child, enumerable: false, writable: true }) // 实例化Child const c1 = new Child() const c2 = new Child() // 实例对象c能够访问到Parent构造函数的实例属性以及Parent原型对象的属性 console.log(c1.array) // [1, 2, 3] console.log(c2.array2) // [4, 5 ,6] c1.say() c2.say() /* 重点来啦 */ // 尝试改变对象继承的属性 c1.name = 'zs' c1.array.push(4) c1.array2.push(7) // 会发现实例对象c2继承的引用属性发生了改变 console.log(c2.name) // 仍然是gsq console.log(c2.array) // [1, 2, 3, 4] console.log(c2.array2) // [4, 5 ,6 ,7]
-
优点:实现了原型对象属性与方法的继承
-
缺点:存在引用值共享的问题,无法实现多继承
-
-
构造函数继承
-
子类构造函数借用父类构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// 父类构造函数 function Parent(){ // 实例属性(引用类型) this.array = [1, 2, 3] } // 原型属性 Parent.prototype.title = 'gsq' // 子类构造函数 function Son(){ // 借用父类的构造函数 // 变更Parent执行时内部的this指向为实例对象 Parent.call(this) } // 实例化 const s1 = new Son() const s2 = new Son() // 解决了引用值不会共享的问题 s1.array.push(4) console.log(s2.array) // [1,2,3] // 但访问不了父类原型对象的属性与方法 s2.title // undefined
-
优点:解决了原型链继承引用值共享的问题,可以实现多继承
-
缺点:只能继承父类的实例属性和方法,不能继承原型对象的属性和方法
-
-
组合继承(伪经典继承)
-
原型链继承结合构造函数继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
function Parent(){ // 实例属性(引用类型) this.array = [1, 2, 3] } // 原型方法 Parent.prototype.say = function (){ console.log("say sth") } // 构造函数继承 function Son(){ Parent.call(this) } // 原型链继承 Son.prototype = new Parent() const s1 = new Son() const s2 = new Son() // 不会出现引用类型属性共享的问题 s1.array.push(4) console.log(s2.array) // [1,2,3] // 同时也可以访问父类原型上的方法 s2.say() // "say sth"
-
优点:解决原型链继承和构造函数继承的问题
-
缺点:父类的构造函数复用的问题(执行两次)
-
-
寄生组合继承(经典继承)
-
利用
Object.create()
方法,指定子类原型对象的父类原型对象(不再通过父类实例) -
var B = Object.create(A)
:以A
对象为原型,生成了B
对象。B
继承了A
的所有属性和方法。1 2 3 4 5 6 7 8 9 10 11 12
function Parent(){} function Son(){ Parent.call(this) } // Son的原型直接继承Parent的原型 Son.prototype = Object.create(Parent.prototype) // Object.create等同于 Object.create = function (proto) { function F() {} F.prototype = proto; return new F(); };
-
优点:优化了组合继承中父类构造函数的复用的问题
-
缺点:重写了子类的原型,会导致子类的属性与方法无法访问
-
-
ES6 继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
class Father { age = 60; gender = "male"; constructor(age) { this.age = age; } getAge() { return this.age; } static getName() { return "father"; } } class Son extends Father { constructor(name, age) { /* 子类必须在constructor方法中调用super方法,否则新建实例时会报错。 这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法, 然后再对其进行加工,加上子类自己的实例属性和方法。 如果不调用super方法,子类就得不到this对象。 */ super(age); this.name = name; } } const son = new Son("gsq", 90); console.log(son); Son { age: 90, gender: 'male', name: 'gsq' }
-
-
变态面试题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
// 构造函数 function Foo(){ // 全局函数 getName = function(){ cosole.log(1) } return this } // 静态方法 Foo.getName = function(){ console.log(2) } // 原型方法 Foo.prototype.getName = function (){ console.log(3) } // 函数表达式 var getName = function (){ console.log(4) } // 函数声明 function getName(){ console.log(5) } /*-------------------------*/ // 第一题 console.log(Foo())// 独立调用执行,返回值this指向window对象 // window Foo.getName()
9 call、bind和apply三者关系
- 他们都是用来改变函数this指向的
- call和apply是直接进行函数调用的,而bind不会执行相关函数,而是返回一个新函数,此新函数已经自动绑定了新的this指向
- call和apply的区别主要在于参数设定上
- 以下三行代码是等价的
|
|
10 判断数据类型
-
1.typeof
1 2 3 4 5 6 7 8 9 10 11
console.log(typeof 1); // number console.log(typeof true); // boolean console.log(typeof 'mc'); // string console.log(typeof Symbol) // function console.log(typeof function(){}); // function console.log(typeof console.log()); // undefined console.log(typeof []); // object console.log(typeof {}); // object console.log(typeof null); // object console.log(typeof undefined); // undefined 复制代码
优点:能够快速区分基本数据类型
缺点:不能将Object、Array和Null区分,都返回object
2.instanceof
1 2 3 4 5 6 7
console.log(1 instanceof Number); // false console.log(true instanceof Boolean); // false console.log('str' instanceof String); // false console.log([] instanceof Array); // true console.log(function(){} instanceof Function); // true console.log({} instanceof Object); // true 复制代码
优点:能够区分Array、Object和Function,适合用于判断自定义的类实例对象
缺点:Number,Boolean,String基本数据类型不能判断
3.Object.prototype.toString.call()
1 2 3 4 5 6 7 8 9 10
var toString = Object.prototype.toString; console.log(toString.call(1)); //[object Number] console.log(toString.call(true)); //[object Boolean] console.log(toString.call('mc')); //[object String] console.log(toString.call([])); //[object Array] console.log(toString.call({})); //[object Object] console.log(toString.call(function(){})); //[object Function] console.log(toString.call(undefined)); //[object Undefined] console.log(toString.call(null)); //[object Null] 复制代码
优点:精准判断数据类型
缺点:写法繁琐不容易记,推荐进行封装后使用
11 Promise
promise。then返回的是什么
then中有return的话,return的值有两种情况 1.除Promise对象外,则默认成功 2.Promise对象,则下一个then是处理这个Promise对象的
Author gsemir
LastMod 2021-03-21