JS前端面试题(二)

文章目录

31.说说你对promise的了解

  • 依照Promise/A+的定义,Promise有四种状态:
    • pending:初始状态, 非fulfilledrejected.
    • fulfilled:成功的操作.
    • rejected:失败的操作.
    • settled: Promise已被fulfilledrejected,且不是pending
  • 另外,fulfilledrejected一起合称settled
  • Promise对象用来进行延迟(deferred) 和异步(asynchronous) 计算

Promise 的构造函数

  • 构造一个Promise,最基本的用法如下:
var promise = new Promise(function(resolve, reject) {
    
    
        if (...) {
    
      // succeed
            resolve(result);
        } else {
    
       // fails
            reject(Error(errMessage));
        }
    });
  • Promise实例拥有then方法(具有then方法的对象,通常被称为thenable)。它的使用方法如下:

    1promise.then(onFulfilled, onRejected)

  • 接收两个函数作为参数,一个在fulfilled的时候被调用,一个在rejected的时候被调用,接收参数就是futureonFulfilled对应resolve,onRejected对应reject

32.谈谈你对AMD、CMD的理解

  • CommonJS是服务器端模块的规范,Node.js采用了这个规范。CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数
  • AMD推荐的风格通过返回一个对象做为模块对象,CommonJS的风格通过对module.exportsexports的属性赋值来达到暴露模块对象的目的

33.web开发中会话跟踪的方法有哪些

  • cookie
  • session
  • url重写
  • 隐藏input
  • ip地址

34.介绍js有哪些内置对象?

  • ObjectJavaScript中所有对象的父对象
  • 数据封装类对象:ObjectArrayBooleanNumberString
  • 其他对象:FunctionArgumentsMathDateRegExpError

35.说几条写JavaScript的基本规范?

  • 不要在同一行声明多个变量
  • 请使用===/!==来比较true/false或者数值
  • 使用对象字面量替代new Array这种形式
  • 不要使用全局函数
  • Switch语句必须带有default分支
  • If语句必须使用大括号
  • for-in循环中的变量 应该使用var关键字明确限定作用域,从而避免作用域污

36.javascript创建对象的几种方式?

javascript创建对象简单的说,无非就是使用内置对象或各种自定义对象,当然还可以用JSON;但写法有很多种,也能混合使用

  • 对象字面量的方式
person={
    
    firstname:"Mark",lastname:"Yun",age:25,eyecolor:"black"};
  • function来模拟无参的构造函数
function Person(){
    
    }
    var person=new Person();//定义一个function,如果使用new"实例化",该function可以看作是一个Class
        person.name="Mark";
        person.age="25";
        person.work=function(){
    
    
        alert(person.name+" hello...");
    }
person.work();
  • function来模拟参构造函数来实现(用this关键字定义构造的上下文属性)
function Pet(name,age,hobby){
    
    
       this.name=name;//this作用域:当前对象
       this.age=age;
       this.hobby=hobby;
       this.eat=function(){
    
    
          alert("我叫"+this.name+",我喜欢"+this.hobby+",是个程序员");
       }
    }
    var maidou =new Pet("麦兜",25,"coding");//实例化、创建对象
    maidou.eat();//调用eat方法
  • 用工厂方式来创建(内置对象)
var wcDog =new Object();
     wcDog.name="旺财";
     wcDog.age=3;
     wcDog.work=function(){
    
    
       alert("我是"+wcDog.name+",汪汪汪......");
     }
     wcDog.work();
  • 用原型方式来创建
function Dog(){
    
    }
Dog.prototype.name="旺财";
Dog.prototype.eat=function(){
    
    
 alert(this.name+"是个吃货");
}
var wangcai =new Dog();
wangcai.eat();
  • 用混合方式来创建
function Car(name,price){
    
    
 this.name=name;
 this.price=price; 
}
Car.prototype.sell=function(){
    
    
  alert("我是"+this.name+",我现在卖"+this.price+"万元");
 }
var camry =new Car("凯美瑞",27);
camry.sell(); 

37.eval是做什么的?

  • 它的功能是把对应的字符串解析成JS代码并运行
  • 应该避免使用eval,不安全,非常耗性能(2次,一次解析成js语句,一次执行)
  • JSON字符串转换为JSON对象的时候可以用eval,var obj =eval('('+ str +')')

38.null,undefined 的区别?

  • undefined表示不存在这个值。
  • undefined:是一个表示”无”的原始值或者说表示”缺少值”,就是此处应该有一个值,但是还没有定义。当尝试读取时会返回undefined
  • 例如变量被声明了,但没有赋值时,就等于undefined
  • null表示一个对象被定义了,值为“空值”
  • null: 是一个对象(空对象, 没有任何属性和方法)
  • 例如作为函数的参数,表示该函数的参数不是对象;
  • 在验证null时,一定要使用 ===,因为==无法分别null和 undefined

39.[“1”, “2”, “3”].map(parseInt) 答案是多少?

  • [1, NaN, NaN]因为parseInt需要两个参数(val, radix),其中radix表示解析时用的基数。
  • map传了3(element, index, array),对应的radix不合法导致解析失败。

40.javascript 代码中的”use strict”;是什么意思 ? 使用它区别是什么?

  • use strict是一种ECMAscript 5添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行,使JS编码更加规范化的模式,消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为

41.js延迟加载的方式有哪些?

  • deferasync、动态创建DOM方式(用得最多)、按需异步载入js

42.defer和async

  • defer并行加载js文件,会按照页面上script标签的顺序执行
  • async并行加载js文件,下载完成立即执行,不会按照页面上script标签的顺序执行

43.说说严格模式的限制

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 禁止this指向全局对象

44.attribute和property的区别是什么?

  • attributedom元素在文档中作为html标签拥有的属性;
  • property就是dom元素在js中作为对象拥有的属性。
  • 对于html的标准属性来说,attributeproperty是同步的,是会自动更新的
  • 但是对于自定义的属性来说,他们是不同步的

45.ECMAScript6 怎么写class么,为什么会出现class这种东西?

  • 这个语法糖可以让有OOP基础的人更快上手js,至少是一个官方的实现了
  • 但对熟悉js的人来说,这个东西没啥大影响;一个Object.creat()搞定继承,比class简洁清晰的多

46.常见兼容性问题

  • png24位的图片在iE6浏览器上出现背景,解决方案是做成PNG8
  • 浏览器默认的marginpadding不同。解决方案是加一个全局的*{margin:0;padding:0;}来统一,,但是全局效率很低,一般是如下这样解决:
  • IE下,event对象有x,y属性,但是没有pageX,pageY属性
  • Firefox下,event对象有pageX,pageY属性,但是没有x,y属性.

47.函数防抖节流的原理

防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于设置的时间,防抖的情况下只会调用一次,而节流的情况会每隔一定时间调用一次函数。

防抖(debounce): n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间

function debounce(func, wait, immediate=true) {
    
    
    let timeout, context, args;
        // 延迟执行函数
        const later = () => setTimeout(() => {
    
    
            // 延迟函数执行完毕,清空定时器
            timeout = null
            // 延迟执行的情况下,函数会在延迟函数中执行
            // 使用到之前缓存的参数和上下文
            if (!immediate) {
    
    
                func.apply(context, args);
                context = args = null;
            }
        }, wait);
        let debounced = function (...params) {
    
    
            if (!timeout) {
    
    
                timeout = later();
                if (immediate) {
    
    
                    //立即执行
                    func.apply(this, params);
                } else {
    
    
                    //闭包
                    context = this;
                    args = params;
                }
            } else {
    
    
                clearTimeout(timeout);
                timeout = later();
            }
        }
    debounced.cancel = function () {
    
    
        clearTimeout(timeout);
        timeout = null;
    };
    return debounced;
};

防抖的应用场景:

  • 每次 resize/scroll 触发统计事件
  • 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)

节流(throttle): 高频事件在规定时间内只会执行一次,执行一次后,只有大于设定的执行周期后才会执行第二次。

//underscore.js
function throttle(func, wait, options) {
    
    
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {
    
    };

    var later = function () {
    
    
        previous = options.leading === false ? 0 : Date.now() || new Date().getTime();
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function () {
    
    
        var now = Date.now() || new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
    
    
            if (timeout) {
    
    
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
    
    
            // 判断是否设置了定时器和 trailing
            timeout = setTimeout(later, remaining);
        }
        return result;
    };

    throttled.cancel = function () {
    
    
        clearTimeout(timeout);
        previous = 0;
        timeout = context = args = null;
    };

    return throttled;
};

函数节流的应用场景有:

  • DOM 元素的拖拽功能实现(mousemove)
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • 计算鼠标移动的距离(mousemove)
  • Canvas 模拟画板功能(mousemove)
  • 搜索联想(keyup)
  • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次

48.原始类型有哪几种?null是对象吗?48.原始类型有哪几种?null是对象吗?

.js中5种原始数据类型

​ number:整数/小数/NaN
string:
boolean:
null:
undefined:

Null类型是第二个只有一个值的数据类型,这个特殊的值是null,从逻辑角度来看,null值表示一个空对象指针,而这也正是使用typeof操作符检测null值会返回“object”的原因

49.为什么console.log(0.2+0.1==0.3) //false

JavaScript 中的 number 类型就是浮点型,JavaScript 中的浮点数采用IEEE-754 格式的规定,这是一种二进制表示法,可以精确地表示分数,比如 1/2,1/8,1/1024,每个浮点数占 64 位。但是,二进制浮点数表示法并不能精确的表示类似 0.1 这样 的简单的数字,会有舍入误差。
由于采用二进制,JavaScript 也不能有限表示 1/10、1/2 等这样的分数。在二进制中,1/10(0.1)被表示为 0.00110011001100110011…… 注意 0011 是无限重复的,这是舍入误差造成的,所以对于 0.1 + 0.2 这样的运算,操作数会先被转成二进制,然后再计算

50.说一下JS中类型转换的规则?

转数字 转字符串 转布尔值
undefined NaN “undefined” false
null 0 “null” false
true 1 “true”
false 0 “false”
0 “0” false
-0 “0” false
NaN “NaN” false
Infinity “Infinity” true
-Infinity ”-Infinity” true
1(非零) “1” true
{}(任意对象) 见下文 见下文 true
0 ”” true
9 9 “9” true
”a” NaN 使用.join()方法 true
function(){}(任意函数) NaN 见下文 true

Number的原始类型转换规则

  • 数值转换后还是数值
  • 字符串如果可以解析为数值则为数值, 空字符串为0, 无法解析的字符串为NaN
  • 布尔转数值, true转为1, false转为0
  • null转换为0

img

原始类型转换Number

Number的对象类型转换规则

传入实例M, 先调用M的valueOf(), 如果返回值V为基本数据类型, 则直接使用Number(V), 求最终返回值
如果T不属于基本数据类型, 则调用M的toString(), 如果返回值S为基本数据类型, 则直接使用Number(S),求最后的结果, 如果S不属于基本数据类型, 则直接返回NaN

img

对象类型转换1

img

对象类型转换2

String的原始类型转换规则

  • 数值(Number)转为相应的字符串
  • 字符串(String) 转换后还是字符串
  • 布尔值(Boolean)转换规则: true => ‘true’, false=> ‘false’
  • undefine 转换为"undefine"
  • null 转换为’null’

img

String原始类型转换

String 的对象类型转换规则

与Number的对象转换规则类似, 区别是: 先调用对象的toString(), 然后再调用valueOf()
其实正常情况下, 对象调用自身的toString()后, 对象就可以转换为string基本类型, valueOf() 没有机会被调用, 但万事有个例, 如果我们重新定义了对象的toString()方法,使其返回非基本类型的值, 那样就有机会调用对象的valueOf()方法了

img

String对象类型转换规则

Boolean的原始类型转换 和 对象类型转换

undefined,null,NaN,'',-0,+0皆为false, 其余为true

隐式类型转换

四则运算+, -, *, /

img

隐式类型转换之四则运算

51.深拷贝和浅拷贝的区别?如何实现

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。浅拷贝只复制对象的第一层属性
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。对对象的属性进行递归复制

浅拷贝 实现方法

1、可以通过简单的赋值实现

类似上面的例子,当然,我们也可以封装一个简单的函数,如下:

 function simpleClone(initalObj) {
    
        
      var obj = {
    
    };    
      for ( var i in initalObj) {
    
    
        obj[i] = initalObj[i];
      }    
      return obj;
    }

    var obj = {
    
    
      a: "hello",
      b:{
    
    
          a: "world",
          b: 21
        },
      c:["Bob", "Tom", "Jenny"],
      d:function() {
    
    
          alert("hello world");
        }
    }
    var cloneObj = simpleClone(obj); 
    console.log(cloneObj.b); 
    console.log(cloneObj.c);
    console.log(cloneObj.d);

    cloneObj.b.a = "changed";
    cloneObj.c = [1, 2, 3];
    cloneObj.d = function() {
    
     alert("changed"); };
    console.log(obj.b);
    console.log(obj.c);
    console.log(obj.d);

2、Object.assign()实现

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

var obj = {
    
     a: {
    
    a: "hello", b: 21} };

var initalObj = Object.assign({
    
    }, obj);

initalObj.a.a = "changed";

console.log(obj.a.a); //  "changed"

注意:当object只有一层的时候,是深拷贝,例如如下:

var obj1 = {
    
     a: 10, b: 20, c: 30 };
var obj2 = Object.assign({
    
    }, obj1);
obj2.b = 100;
console.log(obj1);
// { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(obj2);
// { a: 10, b: 100, c: 30 }

深拷贝的实现方式

1、对象只有一层的话可以使用上面的:Object.assign()函数

2、转成 JSON 再转回来

var obj1 = {
    
     body: {
    
     a: 10 } };
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.body.a = 20;
console.log(obj1);
// { body: { a: 10 } } <-- 沒被改到
console.log(obj2);
// { body: { a: 20 } }
console.log(obj1 === obj2);
// false
console.log(obj1.body === obj2.body);
// false

用JSON.stringify把对象转成字符串,再用JSON.parse把字符串转成新的对象。

可以封装如下函数

var cloneObj = function(obj){
    var str, newobj = obj.constructor === Array ? [] : {};
    if(typeof obj !== 'object'){
        return;
    } else if(window.JSON){
        str = JSON.stringify(obj), //系列化对象
        newobj = JSON.parse(str); //还原
    } else {
        for(var i in obj){
            newobj[i] = typeof obj[i] === 'object' ? 
            cloneObj(obj[i]) : obj[i]; 
        }
    }
    return newobj;
};

3、递归拷贝

function deepClone(initalObj, finalObj) {
    
        
  var obj = finalObj || {
    
    };    
  for (var i in initalObj) {
    
            
    var prop = initalObj[i];        // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况
    if(prop === obj) {
    
                
      continue;
    }        
    if (typeof prop === 'object') {
    
    
      obj[i] = (prop.constructor === Array) ? [] : {
    
    };            
      arguments.callee(prop, obj[i]);
    } else {
    
    
      obj[i] = prop;
    }
  }    
  return obj;
}
var str = {
    
    };
var obj = {
    
     a: {
    
    a: "hello", b: 21} };
deepClone(obj, str);
console.log(str.a);

4、使用Object.create()方法

直接使用var newObj = Object.create(oldObj),可以达到深拷贝的效果。

function deepClone(initalObj, finalObj) {
    
        
  var obj = finalObj || {
    
    };    
  for (var i in initalObj) {
    
            
    var prop = initalObj[i];        // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况
    if(prop === obj) {
    
                
      continue;
    }        
    if (typeof prop === 'object') {
    
    
      obj[i] = (prop.constructor === Array) ? [] : Object.create(prop);
    } else {
    
    
      obj[i] = prop;
    }
  }    
  return obj;
}

5、jquery

jquery 有提供一个$.extend可以用来做 Deep Copy。

var $ = require('jquery');
var obj1 = {
    
    
    a: 1,
    b: {
    
     f: {
    
     g: 1 } },
    c: [1, 2, 3]
};
var obj2 = $.extend(true, {
    
    }, obj1);
console.log(obj1.b.f === obj2.b.f);
// false

6、lodash

另外一个很热门的函数库lodash,也有提供_.cloneDeep用来做 Deep Copy。

var _ = require('lodash');
var obj1 = {
    
    
    a: 1,
    b: {
    
     f: {
    
     g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false

这个性能还不错,使用起来也很简单。

52.如何判断this?箭头函数的this是什么

1.谁作为拥有者调用它就指向谁

function a() {
    
     
    console.log(this); 
}
var b  = {
    
    };
b.hehe = a;
b.hehe();
//这时候this指向b//常见的就是绑定事件

2.bind谁就指向谁

function a() {
    
     
    console.log(this); 
}
var b  = {
    
    };
var c = {
    
    };
b.hehe = a.bind(c);
b.hehe();
//这时候this指向c//如果你用bind的话

3.没有拥有者,直接调用,就指向window

function a() {
    
     
    console.log(this); 
}
a();
//this指向window

4.call谁就是谁,apply谁就是谁,其实bind就是通过call和apply实现的


箭头函数不绑定this,会捕获其所在的上下文的this值,作为自己的this值

53.== 和 ===的区别

1.===:三个等号我们称为等同符,当等号两边的值为相同类型的时候,直接比较等号两边的值,值相同则返回true,若等号两边的值类型不同时直接返回false。

例:100===“100” //返回false

abc===“abc” //返回false

‘abc’===“abc” //返回true

NaN===NaN //返回false

false===false //返回true

2.==:两个等号我们称为等值符,当等号两边的值为相同类型时比较值是否相同,类型不同时会发生类型的自动转换,转换为相同的类型后再作比较。

54.什么是闭包

  • 闭包就是能够读取其他函数内部变量的函数
  • 闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域
  • 闭包的特性:
    • 函数内再嵌套函数
    • 内部函数可以引用外层的参数和变量
    • 参数和变量不会被垃圾回收机制回收

说说你对闭包的理解

  • 使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。在js中,函数即闭包,只有函数才会产生作用域的概念
  • 闭包 的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中
  • 闭包的另一个用处,是封装对象的私有属性和私有方法
  • 好处:能够实现封装和缓存等;
  • 坏处:就是消耗内存、不正当使用会造成内存溢出的问题

使用闭包的注意点

  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露
  • 解决方法是,在退出函数之前,将不使用的局部变量全部删除

55.JavaScript原型,原型链 ? 有什么特点?

  • 每个对象都会在其内部初始化一个属性,就是prototype(原型),当我们访问一个对象的属性时
  • 如果这个对象内部不存在这个属性,那么他就会去prototype里找这个属性,这个prototype又会有自己的prototype,于是就这样一直找下去,也就是我们平时所说的原型链的概念
  • 关系:instance.constructor.prototype = instance.__proto__
  • 特点:
    • JavaScript对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变
  • 当我们需要一个属性的时,Javascript引擎会先看当前对象中是否有这个属性, 如果没有的
  • 就会查找他的Prototype对象是否有这个属性,如此递推下去,一直检索到Object内建对象

56.typeof()和instanceof()的用法区别

typeof() 是一个一元运算,放在一个运算数之前,运算数可以是任意类型。
它返回值是一个字符串,该字符串说明运算数的类型。
** instanceof() ** 运算符用来判断一个构造函数的prototype属性所指向的对象是否存在另外一个要检测对象的原型链上。通常来讲,使用 instanceof 就是判断一个实例是否属于某种类型。

57.什么是变量提升

变量提升:函数声明和变量声明总是会被解释器悄悄地被"提升"到方法体的最顶部。

JavaScript 只有声明的变量会提升,初始化的不会。

var x =5;// 初始化 x  
var y =7;// 初始化 y  
  
elem = document.getElementById("demo");// 查找元素  
elem.innerHTML = x +" "\+ y;  // 5 7
varx =5;// 初始化 x  
  
elem = document.getElementById("demo");// 查找元素  
elem.innerHTML = x +" "\+ y;           // 显示 x 和 y  
  
vary =7;// 初始化 y

输出结果:x 为:5,y 为:undefined

y 输出了undefined,这是因为变量声明 (var y) 提升了,但是初始化(y = 7) 并不会提升,所以 y 变量是一个未定义的变量。
类似下列代码

var x = 5; // 初始化 x
var y;     // 声明 y

elem = document.getElementById("demo"); // 查找元素
elem.innerHTML = x + " " + y;           // 显示 x 和 y

y = 7;    // 设置 y 为 7

1.函数声明会被提升 对于函数表达式,是不会提升的

1         //函数声明, 形如:
2         function show(){
    
    
3             console.log( '函数声明方式' );
4         }
5 
6         //函数表达式, 形如:
7         var show = function(){
    
    
8             console.log( '表达式方式' );
9         }

2.出现同名的函数声明,变量声明的时候, 函数声明会被优先提升,变量声明会被忽略

show(); //你好
var show;
function show(){
    
    
    console.log( '你好' );
}
show = function(){
    
    
    console.log( 'hello' );
}

上面这段代码,结果为什么会是 ‘你好’?
当出现同名的函数声明,变量声明的时候, 函数声明会被优先提升,变量声明会被忽略。 所以经过编译之后,就变成:

function show(){
    
    
    console.log( '你好' );
}
show(); //你好
show = function(){
    
    
    console.log( 'hello' );
}
show();//如果这里在调用一次,就是hello, 因为show函数体在执行阶段 被 重新赋值了

3.如果有同名的函数声明,后面的会覆盖前面的

show(); //how are you
var show;
function show(){
    
    
    console.log( 'hello' );
}    
show = function(){
    
    
    console.log( '你好' );
}
function show(){
    
    
    console.log( 'how are you!' );
}
//上面的代码经过编译之后,变成如下形式:
function show(){
    
    
    console.log( 'how are you!' );
}
show(); //how are you
show = function(){
    
    
    console.log( '你好' );
}
show(); //如果在这里再执行一次,结果:你好

58.all、apply以及bind函数内部实现是怎么样的

call, apply, bind都是改变函数执行的上下文,说的直白点就是改变了函数this的指向。不同的是:call和apply改变了函数的this,并且执行了该函数,而bind是改变了函数的this,并返回一个函数,但不执行该函数。

看下面的例子1:

var doThu = function(a, b) {
    
    
    console.log(this)
    console.log(this.name)
    console.log([a, b])
}
var stu = {
    
    
    name: 'xiaoming',
    doThu: doThu,
}
stu.doThu(1, 2) // stu对象 xiaoming [1, 2]
doThu.call(stu, 1, 2) // stu对象 xiaoming [1, 2]

由此可见,在stu上添加一个属性doThu,再执行这个函数,就将doThu的this指向了stu。而call的作用就与此相当,只不过call为stu添加了doThu方法后,执行了doThu,然后再将doThu这个方法从stu中删除。

下面来看call函数的内部实现原理:

Function.prototype.call = function(thisArg, args) {
    
    
    // this指向调用call的对象
    if (typeof this !== 'function') {
    
     // 调用call的若不是函数则报错
        throw new TypeError('Error')
    }
    thisArg = thisArg || window
    thisArg.fn = this   // 将调用call函数的对象添加到thisArg的属性中
    const result = thisArg.fn(...[...arguments].slice(1)) // 执行该属性
    delete thisArg.fn   // 删除该属性
    return result
}

apply的实现原理和call一样,只不过是传入的参数不同而已。下面只给出代码,不做解释:

Function.prototype.apply = function(thisArg, args) {
    
    
    if (typeof this !== 'function') {
    
     
        throw new TypeError('Error')
    }
    thisArg = thisArg || window
    thisArg.fn = this
    let result
    if(args) {
    
    
        result = thisArg.fn(...args)
    } else {
    
    
        result = thisArg.fn()
    }
    delete thisArg.fn
    return result
}

bind的实现原理比call和apply要复杂一些,bind中需要考虑一些复杂的边界条件。bind后的函数会返回一个函数,而这个函数也可能被用来实例化:

Function.prototype.bind = function(thisArg) {
    
    
    if(typeof this !== 'function'){
    
    
        throw new TypeError(this + 'must be a function');
    }
    // 存储函数本身
    const _this  = this;
    // 去除thisArg的其他参数 转成数组
    const args = [...arguments].slice(1)
    // 返回一个函数
    const bound = function() {
    
    
        // 可能返回了一个构造函数,我们可以 new F(),所以需要判断
        if (this instanceof bound) {
    
    
            return new _this(...args, ...arguments)
        }
        // apply修改this指向,把两个函数的参数合并传给thisArg函数,并执行thisArg函数,返回执行结果
        return _this.apply(thisArg, args.concat(...arguments))
    }
    return bound
}

59.为什么会出现setTimeout倒计时误差?如何减少

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	<body>
		<p id="message"></p>
	</body>
	<script type="text/javascript">
		var message = document.getElementById("message");
		var count = 1000;
		function animate() {
    
    
			var start = +new Date();
			message.innerHTML = count--;
			var finish = +new Date();
			setTimeout(animate, 1000 - (finish-start));
			
		}
		animate();
	</script>
</html>

本例实现了倒数1000秒的功能,我们在使用setTimeout的时候如果要一个函数每每隔1秒执行一次就会这样写

setTimeout(animate, 1000);
但是这样会忽略animate方法本身的运行时间,所以我们可以在执行animate方法的时候计算这个方法主要的语句的执行时间,之后在setTimeout中减去那个由于运行语句而耽搁的时间,从而实现更加精确的计时

60.谈谈你对JS执行上下文栈和作用域链的理解

执行上下文就是当前 JavaScript 代码被解析和执行时所在环境, JS执行上下文栈可以认为是一个存储函数调用的栈结构,遵循先进后出的原则。

  • JavaScript执行在单线程上,所有的代码都是排队执行。
  • 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
  • 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行-完成后,当前函数的执行上下文出栈,并等待垃圾回收。
  • 浏览器的JS执行引擎总是访问栈顶的执行上下文。
  • 全局上下文只有唯一的一个,它在浏览器关闭时出栈。

作用域链: 无论是 LHS 还是 RHS 查询,都会在当前的作用域开始查找,如果没有找到,就会向上级作用域继续查找目标标识符,每次上升一个作用域,一直到全局作用域为止。

猜你喜欢

转载自blog.csdn.net/hrj970808/article/details/109645183