相信如果在面试中遇到第一题大部分同学都会做错哈哈,没错它是有坑的,总结了一下面试中可能会出现的一些比较偏僻笔试题(毕竟现在这种环境)虽然猛地一看到这种题可能会一下子夺走你的我
但是会了之后还是会觉得挺有意思的,希望看完你也会有同感。
window中的name属性
const obj = {
name:'猛虎王',
sayHi1:()=>{
console.log(this.name);
},
sayHi2(){
(()=>{
console.log(this.name);
})()
}
}
obj.sayHi1();
obj.sayHi2();
解析: 首先看到箭头函数后我们知道它里面是没有this的,所以我们可以直接把它替换成this
const obj = {
sayHi:this
}
这样就一目了然了,现在的this是指向外面的(全局window,node环境下为global对象) 接下来就是一个比较大的坑了,你是不是会很自信的以为会输出undefined,但结果却是一个空字符串 这是因为在window中也存在一个name属性表示浏览器窗口的名称,默认值是一个空字符串,所以obj.sayHi()会输出一个空的字符串。在node环境下因为外部this是一个空对象所以会是undefined。
在sayHi2方法中也有箭头函数,同样的替换为this
const obj = {
sayHi2(){
this
}
}
很明显当前this是指向调用当前函数的对象,也就是obj,因此obj.sayHi2()会输出猛虎王。
函数剩余参数的书写位置
function fn(list1,...arg,list2){
return [...list1,...arg,list2]
}
fn(['banana','apple'],'orange','pear')
如果这是一道笔试题的话相信大多数人要被这道题所迷惑了,这道题考查的其实是ES6中函数剩余参数的书写位置,即剩余参数之后不能再有其他参数(只能是最后一个参数),否则会报错。 所以答案是会报错。
js继承
继承是可以使得子类具有父类的属性和方法,而不需要再次编写相同的代码的一种概念。
原型链继承
原型链继承可以分为两种,一种是当访问自身不存在的属性或方法时会在原型链上查找,因为所有对象都是由Object
构造出来的,所以说这是一种简单的继承方式。另一种就是当有多个子类的实例对象需要继承父类属性方法的时候:
function Person() {
this.name = '帝皇铠甲'
this.skills = ['五门必杀']
}
Person.prototype.say = function () {
console.log('hello')
}
function Child() {
}
/**
* 关键步:因为实例对象只能访问类自身的属性和方法
* 要想访问父类的属性和方法可以把父类的实例对象挂在子类的原型对象
* 这样子类的实例对象就可以顺着原型链访问父类中的属性和方法
*/
Child.prototype = new Person();
// 由于上面直接把子类原来的原型对象给覆盖,所以需要手动将原型对象中constructor属性指回子类表示子类实例对象的来源
Child.prototype.constructor = Child
let c1 = new Child();
let c2 = new Child();
c1.skills.push('五行终极斩')
console.log(c1, c2)
缺点
- 上述代码中当我们通过c1对象给skiils添加值会发现c2里的skills也发生了变化,这是因为这两个实例使用的是同一个原型对象,内存空间是共享的,这也是原型链继承的缺点,存在引用值共享的问题。
- 所有新实例都会共享父类实例的属性。一个实例修改了原型属性,另一个实例的原型属性也会被修改。
构造函数继承
为了解决原型链属性引用值共享的问题,可以使用call
方法来执行父类构造函数为每个子类的实例对象单独添加属性方法:
// 既然直接使用父类的实例对象会存在引用值共享的问题(因为访问的一直是原型对象中的属性)
// 那我们可以直接为每个子类的实例对象单独生成各自的属性方法
// 这样一来实例对象自身就有了对应的属性和方法就不用再去访问父类的原型对象了
function Person() {
this.name = '帝皇铠甲'
this.skills = ['五门必杀']
}
Person.prototype.say = function () {
console.log('hello')
}
function Child() {
// 关键步骤:使用call方法强制绑定this指向,因为单独的调用this会指向全局对象
Person.call(this)
}
let c1 = new Child();
let c2 = new Child();
c1.skills.push('五行终极斩')
console.log(c1, c2)
c1.say() // c1.say is not a function
缺点
- 父类的构造函数被多次调用,有多少子类的实例对象父类的构造函数就会被调用几次,比较消耗内存。
- 只能继承父类的实例属性和方法,不能继承原型对象中的属性和方法。
组合继承
有没有中继承可以实现两者之间的结合呢?既可以避免引用值的共享问题又可以访问到父类原型对象中的属性和方法,那就是接下来要说的组合继承:
function Person() {
this.name = '帝皇铠甲'
this.skills = ['五门必杀']
}
Person.prototype.say = function () {
console.log('hello')
}
function Child() {
// 父类构造函数第一次调用
Person.call(this)
}
Child.prototype = new Person(); // 父类构造函数第二次调用
Child.prototype.constructor = Child;
let c1 = new Child();
let c2 = new Child();
c1.skills.push('五行终极斩')
console.log(c1, c2)
c1.say() // hello
组合式继承的缺点就是父类构造函数被重复调用,子类的实例对象在实例化的时候会调用两次父类的构造函数。
寄生组合继承
寄生组合继承完美解决了上述问题,并且也是所有继承方式里最优的一种继承方式。
function Person() {
this.name = '帝皇铠甲'
this.skills = ['五门必杀']
}
Person.prototype.say = function () {
console.log('hello')
}
function Child() {
Person.call(this)
}
// 这里我们只需要获取到父类的原型即可所以可以用Object.create方法代替
// Object.creat方法用于创建一个对象,使用现有的对象来提供新创建对象的__proto__
Child.prototype = Object.create(Person.prototype);
Child.prototype.constructor = Child;
let c = new Child();
那么这样一来就解决了只把父类原型赋值给子类原型并减少了父类构造函数重复调用的问题了。
ES6中extends继承
Clas中通过使用extends
关键字实现子类继承父类的属性和方法。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
console.log('is called') // 父类的构造函数必定会被调用一次
}
// 定义在原型中的属性和方法
say() {
console.log("hello")
}
}
class Child extends Person {
constructor() {
super("小鱼", 12)
}
}
const c = new Child()
console.log(c) // {name:"小鱼",age:12}
console.log(c.say()) // hello
为什么子类的构造函数一定要调用super()
?原因就在于ES6的继承机制与ES5完全不同。
ES5中的继承机制是先创建一个独立的子类的实例对象,然后再将父类的属性和方法添加到这个对象上面,即“实例化对象在前,继承在后”。
ES6的继承机制则是将父类的属性和方法先添加到一个空的对象上面,然后再将该对象作为子类的实例对象,即“继承在前,实例化对象在后”。
这就是为什么ES中的继承必须先调用super()
方法,因为这一步会生成一个继承父类的this
对象,没有这一步就无法继承父类。阮一峰
如何判断一个数组是否是稀疏数组?
首先清楚稀疏数组的特点:
- 首先稀疏数组肯定得是一个数组
- 数据元素的总和与数组的长度不相等
const arr=[1,2,3,,,undefined,4]
console.log(arr,arr.length) // [ 1, 2, 3, <2 empty items>, undefined, 4 ] 7
call和apply的应用
let res = console.log.call.call.call.call.call.call.apply((a) => a, [1, 2])
console.log(res)
解析:首先log是一个方法,它可以根据原型链找到Function.prototype
中的call
方法,因为call
本身也没有call
方法所以也会去Function.prototype
中去找,那么中间所有的call
方法实际上可以看作是都在调用一个方法即call
方法,就可以简化成
console.log.call.apply((a)=>a,[1,2])
//等价于
Function.prototype.call.apply((a)=>a,[1,2])
然后接着分析apply
方法,该方法传入了两个参数分别是一个函数和一个数组,既然call
方法调用了apply
方法,那么根据apply
方法改变this
指向的语法应该用apply
方法的第一个参数即传入的函数来调用call
方法并把参数传递进去,由此就演变成了
(a)=>a.call(1,2) // 即 fn.call(1,2)
这下就变成我们所熟悉的了,this
将指向window,将参数传入到调用的函数中就会返回2。
将以下代码转为ES5代码
class Example{
constructor(name){
this.name=name
}
func(){
console.log(this.name)
}
}
首先我们要知道类的一些特点,类是在严格模式下声明的,所以基于这一特点可以直接写出以下代码:
'use strict'
function Example(name){
this.name=name
}
Example.prototype.func=function(){
console.log(this.name)
}
首先我们要知道类的一些特点,它的出现主要是为了解决函数的二义性,函数有两种调用方式一种是直接调用,另一种是通过new
关键字调用。在类中只允许通过new
关键字来调用,这点可以用new.target
来进行判断,如果是通过new
关键字调用的话会返回对应的函数,如果不是则返回 undefined。
'use strict'
function Example(name){
if(!new.target){
throw new Error('Example must be called with new')
}
this.name=name
}
Example.prototype.func=function(){
console.log(this.name)
}
使用类声明还有一个特点就是只有实例属性是可枚举的,原型中的属性不可被枚举,像上面用类声明的代码中func
函数是枚举不出来的,针对这一点可以在函数中使用Object.defineProperty
方法来定义函数中的原型属性。
'use strict'
function Example(name){
if(!new.target){
throw new Error('Example must be called with new')
}
this.name=name
}
Object.defineProperty(Example.prototype,'func',{
value:function(){
console.log(this.name)
},
enumerable:false
})
const e=new Example('hello')
for(let key in e){
console.log(key) // name
}
还有最后一个隐藏比较深的点就是类原型中定义的函数不能通过new
来调用,实现这一点跟上面一样也可以通过使用new.target
来完成。
//完整代码
'use strict'
function Example(name) {
if (!new.target) {
throw new Error('Example must be called with new')
}
this.name = name
}
Object.defineProperty(Example.prototype, 'func', {
value: function () {
if (new.target) {
throw new Error('Example.prototype.func is not a constructor')
}
console.log(this.name)
},
enumerable: false
})
const e = new Example('hello')
new e.func() // 报错 Example.prototype.func is not a constructor
你学到了吗?