JS唬住面试官拿高薪 一 类与对象篇

笔者最近在对原生JS的知识做系统梳理,因为我觉得JS作为前端工程师的根本技术,学再多遍都不为过。打算来做一个系列,以一系列的问题为驱动,当然也会有追问和扩展,内容系统且完整,对初中级选手会有很好的提升,高级选手也会得到复习和巩固。

第一章: 谈谈你对原型链的理解

原型对象和构造函数有何关系?

在JavaScript中,每当定义一个函数数据类型(普通函数、类)时候,都会天生自带一个prototype属性,这个属性指向函数的原型对象
当函数经过new调用时,这个函数就成为了构造函数,返回一个全新的实例对象,这个实例对象有一个__proto__属性,指向构造函数的原型对象。

// 实例.__proto__ === 原型
// 原型.constructor === 构造函数
// 构造函数.prototype === 原型

// 这条线其实是是基于原型进行获取的,可以理解成一条基于原型的映射线。例如: 
const o = new Object()
console.log(o) //{}

// 注意: 其实实例上并不是真正有 constructor 这个指针,它其实是从原型链上获取的
console.log(o.hasOwnProperty('constructor'))  //false 
console.log(o.constructor)                       //[Function: Object]
console.log(o.constructor === Object)            // --> true
console.log(o.__proto__.constructor === Object)  // --> true
console.log(o.__proto__.constructor.prototype.constructor === Object)  // --> true

o.__proto__ = null
console.log(o)                         //[Object: null prototype] {}
console.log(o.constructor === Object)  // --> false

image.png

能不能描述一下原型链?

JavaScript对象通过prototype指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链
image.png

实例对象.__proto__ = 构造函数的.prototype
class A{}
class B extends A{}
class C extends B{}
console.log(new C())

image.png

  • 对象的 hasOwnProperty() 来检查对象自身中是否含有该属性
  • 使用 in 检查对象中是否含有某个属性时,如果对象中没有但是原型链中有,也会返回 true
var obj = {name:'一缕清风'}

'toString' in obj              // true
obj.hasOwnProperty('toString') // false

⚠️**注意: **的是,虽然in能检测到原型链的属性,但 for in 通常却不行。

第二章: JS如何实现继承?

第一种: 借助call,实现构造函数继承
function Parent() {
  this.name = 'parent'
}
Parent.prototype.say = function () {
  console.log("Hello Word")
}

function Child() {
  Parent.call(this)  //继承Parent的属性值
  this.type = 'child'
}

console.log(new Child)   //Child { name: 'parent', type: 'child' }
console.log(new Parent)  //Parent { name: 'parent' }

这样写的时候子类虽然能够拿到父类的属性值,但是问题是父类原型对象中一旦存在方法那么子类无法继承。那么引出下面的方法

第二种: 借助原型链
function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}

function Child() {
  this.type = 'child'
}

Child.prototype = new Parent()
console.log(new Child())  //Parent { type: 'child' }

看似没有问题,父类的方法和属性都能够访问,但实际上有一个潜在的不足。举个例子:

function Parent() {
  this.name = 'parent';
  this.play = [1, 2, 3]
}

function Child() {
  this.type = 'child';
}

Child.prototype = new Parent();

var s1 = new Child();
var s2 = new Child();

s1.play.push(4);
console.log(s1.play, s2.play); //[ 1, 2, 3, 4 ] [ 1, 2, 3, 4 ]

明明我只改变了s1的play属性,为什么s2也跟着变了呢?很简单,因为两个实例使用的是同一个原型对象。
那么还有更好的方式么?

第三种:将前两种组合
function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}

function Child() {
  Parent.call(this) //➕
  this.type = 'child'
}

Child.prototype = new Parent()

var s3 = new Child()
var s4 = new Child()

s3.play.push(4)
console.log(s3.play, s4.play) //[ 1, 2, 3, 4 ] [ 1, 2, 3 ]

之前的问题都得以解决。但是这里又徒增了一个新问题
那就是 Parent3 的构造函数会多执行了一次 Child3.prototype = new Parent3()
这是我们不愿看到的。那么如何解决这个问题?

第四种: 组合继承的优化1
function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}

function Child() {
  Parent.call(this)
  this.type = 'child'
}

Child.prototype = Parent.prototype //➕

var s3 = new Child()
var s4 = new Child()
console.log(s3)  //Parent { name: 'parent', play: [ 1, 2, 3 ], type: 'child' }

这里让将父类原型对象直接给到子类,父类构造函数只执行一次,而且父类属性和方法均能访问,但是我们来测试的时候发现子类实例的构造函数是 Parent4,显然这是不对的,应该是 Child4
image.png

第五种:(最推荐使用): 寄生组合继承的优化2

这是最推荐的一种方式,接近完美的继承,它的名字也叫做寄生组合继承

function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}
Parent.prototype.say = function () {
  console.log("Hello Word")
}


function Child() {
  Parent.call(this)
  this.name = 'child'
}

//- Child.prototype = new Parent()     // 每次都需要new,带来开销,不推荐。其实还可以写成 Child.prototype = new Parent
Child.prototype = Parent.prototype    
Child.prototype.constructor = Child

var child = new Child()
// child.__proto__ = Parent.prototype;
// Object.setPrototypeOf(child, Parent.prototype)
// child.__proto__.constructor = Child

child.say() //Hello Word
console.log(child, child.__proto__)  // Child { name: 'child', play: [ 1, 2, 3 ] } Child { say: [Function] }
console.log(child.constructor, child.__proto__.constructor)  // [Function: Child] [Function: Child]
console.log(Object.getPrototypeOf(child))  // Child { say: [Function] }
console.log(Object.getPrototypeOf(child.__proto__)) //{}

寄生式组合继承写法上和组合继承基本类似,区别是如下这里:

- Dog.prototype = new Parent()
- Child.prototype.constructor = Child

+ function F() {}
+ F.prototype = Parent.prototype
+ let f = new F()
+ f.constructor = Child
+ Child.prototype = f
function Parent() {
  this.name = 'parent'
  this.play = [1, 2, 3]
}
Parent.prototype.say = function () {
  console.log("Hello Word")
}


function Child() {
  Parent.call(this)
  this.name = 'child'
}


// -Child.prototype = Parent.prototype  //直接这样写原型链就不完整
// -Child.prototype.constructor = Child
function F() { }
F.prototype = Parent.prototype
let f = new F()
f.constructor = Child
Child.prototype = f


var child = new Child() 
child.say() //Hello Word
console.log(child, child.__proto__)  // Child {name: "child", play: Array(3)} Parent {constructor: ƒ}
console.log(child.constructor, child.__proto__.constructor)  // [Function: Child] [Function: Child]
console.log(Object.getPrototypeOf(child))  //Child { constructor: [Function: Child] } 
console.log(Object.getPrototypeOf(child.__proto__)) // Parent { say: [Function] }
function F() { }
F.prototype = Parent.prototype
let f = new F()
f.constructor = Child
Child.prototype = f

//-------------------- 对上面👆代码稍微封装后的代码 --------------------

function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}

function inheritPrototype(child, parent) {
    let prototype = object(parent.prototype)
    prototype.constructor = child
    child.prototype = prototype
}
inheritPrototype(Child, Parent)

//------------------------------------------------------------------

var inherit = (function (c, p) {
  var F = function () { }
  return function (c, p) {
    F.prototype = p.prototype
    c.prototype = new F()
    c.uber = p.prototype
    c.prototype.constructor = c
  }
})()

//------------------------------------------------------------------
//如果你嫌弃上面的代码太多了,还可以基于组合继承的代码改成最简单的寄生式组合继承:
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
第六种:借助 Object.create + Object.getPrototypeOf
function Button() {
  this.color = 'red';
}

var button = new Button();
Object.defineProperty(button, 'colorGet', {
  // enumerable默认为false。设为可枚举,不然 Object.create | Object.assign 方法会过滤该属性
  enumerable: true,
  get() {
    return "Could it return " + this.color
  }
});

var circleButton = Object.create(Object.getPrototypeOf(button), Object.getOwnPropertyDescriptors(button));
console.log(circleButton)

ES6的extends被编译后的JavaScript代码
ES6的代码最后都是要在浏览器上能够跑起来的,这中间就利用了babel这个编译工具,将ES6的代码编译成ES5让一些不支持新语法的浏览器也能运行。
那最后编译成了什么样子呢?

function _possibleConstructorReturn(self, call) {
  // ...
  return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}

function _inherits(subClass, superClass) {
  // ...
  //看到没有
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
  if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}


var Parent = function Parent() {
  // 验证是否是 Parent 构造出来的 this
  _classCallCheck(this, Parent);
};

var Child = (function (_Parent) {
  _inherits(Child, _Parent);
  function Child() {
    _classCallCheck(this, Child);

    return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
  }
  return Child;
}(Parent));

核心是_inherits函数,可以看到它采用的依然也是第五种方式 —— 寄生组合继承方式,同时证明了这种方式的成功。不过这里加了一个Object.setPrototypeOf(subClass, superClass),这是用来干啥的呢?
答案是用来继承父类的静态方法。这也是原来的继承方式疏忽掉的地方。

追问: 面向对象的设计一定是好的设计吗?

不一定。从继承的角度说,这一设计是存在巨大隐患的

第七种:class 实现继承
class Animal {
  constructor(name) {
    this.name = name
  }
  getName() {
    return this.name
  }
}
class Dog extends Animal {
  constructor(name, age) {
    super(name)
    this.age = age
  }
}
从设计思想上谈谈继承本身的问题

假如现在有不同品牌的车,每辆车都有drive、music、addOil这三个方法。

class Car {
  constructor(id) {
    this.id = id;
  }

  drive() {
    console.log("drive!");
  }

  music() {
    console.log("music!")
  }

  addOil() {
    console.log("addOil")
  }

}

class OtherCar extends Car { };
const otherCar = new OtherCar();
otherCar.addOil()

现在可以实现车的功能,并且以此去扩展不同的车。
但是问题来了,新能源汽车也是车,但是它并不需要addOil(加油)。
如果让新能源汽车的类继承Car的话,也是有问题的,俗称"大猩猩和香蕉"的问题。大猩猩手里有香蕉,但是我现在明明只需要香蕉,却拿到了一只大猩猩。也就是说加油这个方法,我现在是不需要的,但是由于继承的原因,也给到子类了

继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承

当然你可能会说,可以再创建一个父类啊,把加油的方法给去掉,但是这也是有问题的,一方面父类是无法描述所有子类的细节情况的,为了不同的子类特性去增加不同的父类,代码势必会大量重复,另一方面一旦子类有所变动,父类也要进行相应的更新,代码的耦合性太高,维护性不好

那如何来解决继承的诸多问题呢?
用组合,这也是当今编程语法发展的趋势,比如golang完全采用的是面向组合的设计方式。
顾名思义,面向组合就是先设计一系列零件,然后将这些零件进行拼装,来形成不同的实例或者类。

function drive() {
  console.log("drive!")
}

function music() {
  console.log("music!")
}

function addOil() {
  console.log("addOil")
}

let car = compose(drive, music, addOil)
let newEnergyCar = compose(drive, music)

代码干净,复用性也很好。这就是面向组合的设计方式

第三章: 谈谈你对JS中this的理解

其实JS中的this是一个非常简单的东西,只需要理解它的执行规则就OK
在这里不想像其他博客一样展示太多的代码例子弄得天花乱坠, 反而不易理解
call/apply/bind可以显式绑定, 这里就不说了
主要这些场隐式绑定的场景讨论:

  1. 全局上下文
  2. 直接调用函数
  3. 对象.方法的形式调用
  4. DOM事件绑定(特殊)
  5. new构造函数绑定
  6. 箭头函数
1. 全局上下文

全局上下文默认this指向window, 严格模式下指向 undefined

2. 直接调用函数
let obj = {
  a: function() {
    console.log(this);
  }
}

let func = obj.a;
func();

这种情况是直接调用。this相当于全局上下文的情况

3. 对象.方法的形式调用

还是刚刚的例子,我如果这样写:

obj.a();

这就是 对象.方法() 的情况,this指向这个对象

4. DOM事件绑定

onclick和addEventerListener中 this 默认指向绑定事件的元素
IE比较奇异,使用attachEvent,里面的this默认指向window

5. new+构造函数

此时构造函数中的this指向实例对象

6. 箭头函数?

箭头函数没有this, 因此也不能绑定。里面的this会指向当前最近的非箭头函数的this,找不到就是window(严格模式是undefined)。比如:

let obj = {
  a: function() {
    let do = () => {
      console.log(this);
    }
    do();
  }
}

obj.a(); // 找到最近的非箭头函数a,a现在绑定着obj, 因此箭头函数中的this是obj

优先级: new > call、apply、bind > 对象.方法 > 直接调用

第四章: 能不能模拟实现一个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 = res !== null && typeof res === 'object'
  let isFunction = typeof res === 'function'
  return isObect || isFunction ? res : obj
};

第五章: 能不能模拟实现一个 bind 的效果?

实现bind之前,我们首先要知道它做了哪些事情

  1. 对于普通函数,绑定this指向
  2. 对于构造函数,要保证原函数的原型对象上的属性不能丢失
Function.prototype.bind = function (context, ...args) {
  // 异常处理
  if (typeof this !== "function") {
    throw new Error("Function.prototype.bind - what is trying to be bound is not callable")
  }

  var self = this  // 保存this的值,它代表调用 bind 的函数

  var fNOP = function () { }
  fNOP.prototype = this.prototype
  var bound = function () {
    //这就话的意思是:如果new的话就使用this,函数调用的话就用context
    console.log(this, self, this instanceof self,"输出指向")
    self.apply(this instanceof self ?
      this :
      context, args.concat(Array.prototype.slice.call(arguments)))
  }
  bound.prototype = new fNOP()
  return bound
}

//—————————— 开始测试 ——————————
function A(params) { console.log(this, "输出this") } //{ name: '一缕清风' } 输出this
console.dir(A.bind({ name: "一缕清风" }))
/*
<-  ƒ bound()
    arguments: null
    caller: null
    length: 0
    name: "bound"
    prototype: A
      __proto__:
        constructor: ƒ A(params)
          arguments: null
          caller: null
          length: 1
          name: "A"
          prototype: {constructor: ƒ}
          __proto__: ƒ ()
          [[FunctionLocation]]: VM3559:22
          [[Scopes]]: Scopes[1]
        __proto__: Object
    __proto__: ƒ ()
    [[FunctionLocation]]: VM3559:12
    [[Scopes]]: Scopes[3]
*/

A.bind({ name: "一缕清风" })() //{name: "一缕清风"} "输出this"
var AAA = A.bind({ name: "一缕清风" })
new AAA() //bound {} "输出this"

也可以这么用 Object.create 来处理原型:

Function.prototype.bind = function (context, ...args) {
  if (typeof this !== "function") {
    throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
  }

  var self = this;
  var fbound = function () {
    self.apply(this instanceof self ?
      this :
      context, args.concat(Array.prototype.slice.call(arguments)));
  }
  
  fbound.prototype = Object.create(this.prototype);
  return fbound;
}

第六章: 能不能实现一个 call/apply 函数?

Function.prototype.call = function (obj) {
  let context = obj
  let fn = Symbol('fn')
  context.fn = this
  let args = []
  for (let i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']')
  }
  let result = eval('context.fn(' + args + ')')
  delete context.fn
  return result
}


let obj = { name: "obj" };
function func(x, y) {
  console.log(this)
  return x + y
}
console.log(func.call(obj, 10, 20))

不过我认为换成 ES6 的语法会更精炼一些:

Function.prototype.call = function (obj, ...args) {
  var context = obj || window;
  let fn = Symbol('fn');
  context.fn = this;
  let result = eval('context.fn(...args)');
  delete context.fn
  return result;
}

let obj = { name: "obj" };
function func(x, y) {
  console.log(this)
  return x + y
}
console.log(func.call(obj, 10, 20))

类似的,有apply的对应实现:

Function.prototype.apply = function (obj, args) {
  let context = obj || window;
  context.fn = this;
  let result = eval('context.fn(...args)');
  delete context.fn
  return result;
}


let obj = { name: "obj" };
function func(x, y) {
  console.log(this)
  return x + y
}
console.log(func.apply(obj, [10, 20]))

第七章:实现 Object.create

Object.create2 = function (proto, propertyObject = undefined) {
  if (typeof proto !== 'object' && typeof proto !== 'function') {
    throw new TypeError('Object prototype may only be an Object or null.')
    if (propertyObject == null) {
      new TypeError('Cannot convert undefined or null to object')
    }
    function F() { }
    F.prototype = proto
    const obj = new F()
    if (propertyObject != undefined) {
      Object.defineProperties(obj, propertyObject)
    }
    if (proto === null) {
      // 创建一个没有原型对象的对象,Object.create(null)
      obj.__proto__ = null
    }
    return obj
  }
}

第八章:实现 Object.assign

Object.assign2 = function (target, ...source) {
  if (target == null) {
    throw new TypeError('Cannot convert undefined or null to object')
  }
  let ret = Object(target)
  source.forEach(function (obj) {
    if (obj != null) {
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          ret[key] = obj[key]
        }
      }
    }
  })
  return ret
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值