面试灵魂拷问之JS,请问你头皮发麻没有?

JS数据类型之问—概念篇

1.JS原始数据类型有哪些?引用数据类型有哪些?
在 JS 中,存在着 7 种原始值,分别是:golang

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol
  • bigint
    引用数据类型:
    对象Object(包含普通对象-Object,数组对象-Array,正则对象-RegExp,日期对象-Date,数学函数-Math)
    函数Function
    2.说出下面运行的结果,解释缘由。
function test(person) {
   person.age = 26
   person = {
     name: 'hzj',
     age: 18
   }
   return person
 }
 const p1 = {
   name: 'fyq',
   age: 19
 }
 const p2 = test(p1)
 console.log(p1) // -> ?
 console.log(p2) // -> ?

结果:面试

p1:{name: “fyq”, age: 26}
 p2:{name: “hzj”, age: 18}

缘由: 在函数传参的时候传递的是对象在堆中的内存地址值,test函数中的实参person是p1对象的内存地址,经过调用person.age = 26确实改变了p1的值,但随后person变成了另外一块内存空间的地址,而且在最后将这另一分内存空间的地址返回,赋给了p2。
3.null是对象吗?为何?
结论: null不是对象。
解释: 虽然 typeof null 会输出 object,可是这只是 JS 存在的一个悠久 Bug。在 JS 的最第一版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头表明是对象然而 null 表示为全零,因此将它错误的判断为 object 。
4.'1'.toString()为何能够调用?
其实在这个语句运行的过程当中作了这样几件事情:编程

var s = new String('1');
 s.toString();
 s = null;

第一步: 建立String类实例。
第二步: 调用实例方法。
第三步: 执行完方法当即销毁这个实例。
整个过程体现了基本包装类型的性质,而基本包装类型偏偏属于基本数据类型,包括Boolean, Number和String。数组

参考:《JavaScript高级程序设计(第三版)》P118浏览器

5.0.1+0.2为何不等于0.3?
0.1和0.2在转换成二进制后会无限循环,因为标准位数的限制后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.30000000000000004。babel

第二篇: JS数据类型之问——检测篇

1. typeof 是否能正确判断类型?
对于原始类型来讲,除了 null 均可以调用typeof显示正确的类型。markdown

typeof 1 // 'number'
 typeof '1' // 'string'
 typeof undefined // 'undefined'
 typeof true // 'boolean'
 typeof Symbol() // 'symbol'

但对于引用数据类型,除了函数以外,都会显示"object"。闭包

typeof [] // 'object'
 typeof {} // 'object'
 typeof console.log // 'function'

所以采用typeof判断对象数据类型是不合适的,采用instanceof会更好,instanceof的原理是基于原型链的查询,只要处于原型链中,判断永远为trueapp

const Person = function() {}
 const p1 = new Person()
 p1 instanceof Person // true

 var str1 = 'hello world'
 str1 instanceof String // false

 var str2 = new String('hello world')
 str2 instanceof String // true

2. instanceof可否判断基本数据类型?
能。好比下面这种方式:异步

class PrimitiveNumber {
   static [Symbol.hasInstance](x) {
     return typeof x === 'number'
   }
 }
 console.log(111 instanceof PrimitiveNumber) // true

若是你不知道Symbol,能够看看MDN上关于hasInstance的解释。
其实就是自定义instanceof行为的一种方式,这里将原有的instanceof方法重定义,换成了typeof,所以可以判断基本数据类型。

3. 能不能手动实现一下instanceof的功能
核心: 原型链的向上查找。

function myInstanceof(left, right) {
     //基本数据类型直接返回false
 if(typeof left !== 'object' || left === null) return false;
     //getProtypeOf是Object对象自带的一个方法,可以拿到参数的原型对象
 let proto = Object.getPrototypeOf(left);
     while(true) {
         //查找到尽头,还没找到
 if(proto == null) return false;
         //找到相同的原型对象
 if(proto == right.prototype) return true;
         proto = Object.getPrototypeof(proto);
     }
 }

测试:

console.log(myInstanceof("111", String)); //false
 console.log(myInstanceof(new String("111"), String));//true

4. Object.is和===的区别?
Object在严格等于的基础上修复了一些特殊状况下的失误,具体来讲就是+0和-0,NaN和NaN。 源码以下:

function is(x, y) {
   if (x === y) {
     //运行到1/x === 1/y的时候x和y都为0,可是1/+0 = +Infinity, 1/-0 = -Infinity, 是不同的
 return x !== 0 || y !== 0 || 1 / x === 1 / y;
   } else {
     //NaN===NaN是false,这是不对的,咱们在这里作一个拦截,x !== x,那么必定是 NaN, y 同理
 //两个都是NaN的时候返回true
 return x !== x && y !== y;
   }

第三篇: JS数据类型之问——转换篇

1. [] == ![]结果是什么?为何?
解析:
== 中,左右两边都须要转换为数字而后进行比较。
[]转换为数字为0。
![] 首先是转换为布尔值,因为[]做为一个引用类型转换为布尔值为true,
所以![]为false,进而在转换成数字,变为0。
0 == 0 , 结果为true

2. JS中类型转换有哪几种?

JS中,类型转换只有三种:

  • 转换成数字
  • 转换成布尔值
  • 转换成字符串

转换具体规则以下:

注意"Boolean 转字符串"这行结果指的是 true 转字符串的例子
面试灵魂拷问之JS,请问你头皮发麻没有?

3. == 和 ===有什么区别?

===叫作严格相等,是指:左右两边不只值要相等,类型也要相等,例如'1'===1的结果是false,由于一边是string,另外一边是number。

==不像===那样严格,对于通常状况,只要值相等,就返回true,但==还涉及一些类型转换,它的转换规则以下:

  • 两边的类型是否相同,相同的话就比较值的大小,例如1==2,返回false
  • 判断的是不是null和undefined,是的话就返回true
  • 判断的类型是不是String和Number,是的话,把String类型转换成Number,再进行比较
  • 判断其中一方是不是Boolean,是的话就把Boolean转换成Number,再进行比较
  • 若是其中一方为Object,且另外一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较
    console.log({a: 1} == true);//false
    console.log({a: 1} == "[object Object]");//true

4. 对象转原始类型是根据什么流程运行的?

对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑以下:

  1. 若是Symbol.toPrimitive()方法,优先调用再返回
  2. 调用valueOf(),若是转换为原始类型,则返回
  3. 调用toString(),若是转换为原始类型,则返回
  4. 若是都没有返回原始类型,会报错
    var obj = {
    value: 3,
    valueOf() {
     return 4;
    },
    toString() {
     return '5'
    },
    [Symbol.toPrimitive]() {
     return 6
    }
    }
    console.log(obj + 1); // 输出7

5. 如何让if(a == 1 && a == 2)条件成立?
其实就是上一个问题的应用。

var a = {
   value: 0,
   valueOf: function() {
     this.value++;
     return this.value;
   }
 };
 console.log(a == 1 && a == 2);//true

第四篇: 谈谈你对闭包的理解

什么是闭包?

红宝书(p178)上对于闭包的定义:闭包是指有权访问另一个函数做用域中的变量的函数.

MDN 对闭包的定义为:闭包是指那些可以访问自由变量的函数。 (其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另一个函数做用域中的变量。)

闭包产生的缘由?
首先要明白做用域链的概念,其实很简单,在ES5中只存在两种做用域————全局做用域和函数做用域,当访问一个变量时,解释器会首先在当前做用域查找标示符,若是没有找到,就去父做用域找,直到找到该变量的标示符或者不在父做用域中,这就是做用域链,值得注意的是,每个子函数都会拷贝上级的做用域,造成一个做用域的链条。 好比:

var a = 1;
 function f1() {
   var a = 2
   function f2() {
     var a = 3;
     console.log(a);//3
   }
 }

在这段代码中,f1的做用域指向有全局做用域(window)和它自己,而f2的做用域指向全局做用域(window)、f1和它自己。并且做用域是从最底层向上找,直到找到全局做用域window为止,若是全局尚未的话就会报错。就这么简单一件事情!
闭包产生的本质就是,当前环境中存在指向父级做用域的引用。仍是举上面的例子:

function f1() {
   var a = 2
   function f2() {
     console.log(a);//2
   }
   return f2;
 }
 var x = f1();
 x();

这里x会拿到父级做用域中的变量,输出2。由于在当前环境中,含有对f2的引用,f2偏偏引用了window、f1和f2的做用域。所以f2能够访问到f1的做用域的变量。
那是否是只有返回函数才算是产生了闭包呢?
回到闭包的本质,咱们只须要让父级做用域的引用存在便可,所以咱们还能够这么作:

var f3;
 function f1() {
   var a = 2
   f3 = function() {
     console.log(a);
   }
 }
 f1();
 f3();

让f1执行,给f3赋值后,等于说如今f3拥有了window、f1和f3自己这几个做用域的访问权限,仍是自底向上查找,最近是在f1中找到了a,所以输出2。
在这里是外面的变量f3存在着父级做用域的引用,所以产生了闭包,形式变了,本质没有改变。
闭包有哪些表现形式?
明白了本质以后,咱们就来看看,在真实的场景中,究竟在哪些地方能体现闭包的存在?
返回一个函数。刚刚已经举例。

做为函数参数传递

var a = 1;
 function foo(){ 
  var a = 2;
   function baz(){
     console.log(a);
   }
   bar(baz);
 }
 function bar(fn){
   // 这就是闭包
   fn();
 }
 // 输出2,而不是1
 foo();

在定时器、事件监听、Ajax请求、跨窗口通讯、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

如下的闭包保存的仅仅是window和当前做用域。

// 定时器
 setTimeout(function timeHandler(){
   console.log('111');
 },100)

  // 事件监听
 $('#app').click(function(){
   console.log('DOM Listener');
 })

IIFE(当即执行函数表达式)建立闭包, 保存了全局做用域window和当前函数的做用域,所以能够全局的变量。

var a = 2;
 (function IIFE(){
   // 输出2
 console.log(a);
 })();

如何解决下面的循环输出问题?

for(var i = 1; i <= 5; i ++){
   setTimeout(function timer(){
     console.log(i)
   }, 0)
 }

为何会所有输出6?如何改进,让它输出1,2,3,4,5?(方法越多越好)
由于setTimeout为宏任务,因为JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,所以循环结束后setTimeout中的回调才依次执行,但输出i的时候当前做用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。所以会所有输出6。
解决方法:
一、利用IIFE(当即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中

for(var i = 1;i <= 5;i++){
   (function(j){
     setTimeout(function timer(){
       console.log(j)
     }, 0)
   })(i)
 }

二、给定时器传入第三个参数, 做为timer函数的第一个函数参数

for(var i=1;i<=5;i++){
   setTimeout(function timer(j){
     console.log(j)
   }, 0, i)
 }

三、使用ES6中的let

 for(let i = 1; i <= 5; i++){
   setTimeout(function timer(){
     console.log(i)
   },0)
 }

let使JS发生革命性的变化,让JS有函数做用域变为了块级做用域,用let后做用域链不复存在。代码的做用域以块级为单位,以上面代码为例:

// i = 1
 {
   setTimeout(function timer(){
     console.log(1)
   },0)
 }
 // i = 2
 {
   setTimeout(function timer(){
     console.log(2)
   },0)
 }
 // i = 3
 ...

所以能输出正确的结果。

第五篇: 谈谈你对原型链的理解

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

![](https://s1.51cto.com/images/blog/201910/22/e8aaa00fe932ce3f40821a825274318e.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

2.能不能描述一下原型链?
JavaScript对象经过prototype指向父类对象,直到指向Object对象为止,这样就造成了一个原型指向的链条, 即原型链。
面试灵魂拷问之JS,请问你头皮发麻没有?

对象的 hasOwnProperty() 来检查对象自身中是否含有该属性
使用 in 检查对象中是否含有某个属性时,若是对象中没有可是原型链中有,也会返回 true

第六篇: JS如何实现继承?

第一种: 借助call

function Parent1(){
     this.name = 'parent1';
   }
   function Child1(){
     Parent1.call(this);
     this.type = 'child1'
   }
   console.log(new Child1);

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

function Parent2() {
     this.name = 'parent2';
     this.play = [1, 2, 3]
   }
   function Child2() {
     this.type = 'child2';
   }
   Child2.prototype = new Parent2();

   console.log(new Child2());

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

var s1 = new Child2();
 var s2 = new Child2();
 s1.play.push(4);
 console.log(s1.play, s2.play);

能够看到控制台:
面试灵魂拷问之JS,请问你头皮发麻没有?

明明我只改变了s1的play属性,为何s2也跟着变了呢?很简单,由于两个实例使用的是同一个原型对象。
那么还有更好的方式么?
第三种:将前两种组合

function Parent3 () {
     this.name = 'parent3';
     this.play = [1, 2, 3];
   }
   function Child3() {
     Parent3.call(this);
     this.type = 'child3';
   }
   Child3.prototype = new Parent3();
   var s3 = new Child3();
   var s4 = new Child3();
   s3.play.push(4);
   console.log(s3.play, s4.play);

能够看到控制台:
面试灵魂拷问之JS,请问你头皮发麻没有?

以前的问题都得以解决。可是这里又徒增了一个新问题,那就是Parent3的构造函数会多执行了一次(Child3.prototype = new Parent3();)。这是咱们不肯看到的。那么如何解决这个问题?
第四种: 组合继承的优化1

function Parent4 () {
     this.name = 'parent4';
     this.play = [1, 2, 3];
   }
   function Child4() {
     Parent4.call(this);
     this.type = 'child4';
   }
   Child4.prototype = Parent4.prototype;

这里让将父类原型对象直接给到子类,父类构造函数只执行一次,并且父类属性和方法均能访问,可是咱们来测试一下:

var s3 = new Child4();
 var s4 = new Child4();
 console.log(s3)

面试灵魂拷问之JS,请问你头皮发麻没有?

子类实例的构造函数是Parent4,显然这是不对的,应该是Child4。

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

function Parent5 () {
     this.name = 'parent5';
     this.play = [1, 2, 3];
   }
   function Child5() {
     Parent5.call(this);
     this.type = 'child5';
   }
   Child5.prototype = Object.create(Parent5.prototype);
   Child5.prototype.constructor = Child5;

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

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),这是用来干啥的呢?
答案是用来继承父类的静态方法。这也是原来的继承方式疏忽掉的地方。

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

不必定。从继承的角度说,这一设计是存在巨大隐患的。
从设计思想上谈谈继承自己的问题
假如如今有不一样品牌的车,每辆车都有drive、music、addOil这三个方法。

class Car{
   constructor(id)
 {
     this.id = id;
   }
   drive(){
     console.log("wuwuwu!");
   }
   music(){
     console.log("lalala!")
   }
   addOil(){
     console.log("哦哟!")
   }
 }
 class otherCar extends Car{}

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

继承的最大问题在于:没法决定继承哪些属性,全部属性都得继承。

固然你可能会说,能够再建立一个父类啊,把加油的方法给去掉,可是这也是有问题的,一方面父类是没法描述全部子类的细节状况的,为了避免同的子类特性去增长不一样的父类,代码势必会大量重复,另外一方面一旦子类有所变更,父类也要进行相应的更新,代码的耦合性过高,维护性很差。
那如何来解决继承的诸多问题呢?
用组合,这也是当今编程语法发展的趋势,好比golang彻底采用的是面向组合的设计方式。
顾名思义,面向组合就是先设计一系列零件,而后将这些零件进行拼装,来造成不一样的实例或者类。

function drive(){
   console.log("wuwuwu!");
 }
 function music(){
   console.log("lalala!")
 }
 function addOil(){
   console.log("哦哟!")
 }
 let car = compose(drive, music, addOil);
 let newEnergyCar = compose(drive, music);

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

相关文章
相关标签/搜索