【必看】JavaScript面试题集合(二)

目录:

1.typeof 与instanceof 的区别

typeof和instanceof都可以用来判断变量,但它们的用法有很大区别

  • typeof只能判断基本数据类型,返回的值就是该变量的基本数据类型(字符串)
  • instanceof用来判断引用数据类型,比如判断对象,返回的是个布尔值

例子:

使用typeof判断变量是否存在

console.log(typeof(1));//number
console.log(typeof("abc"));//string
console.log(typeof(true));//boolean
console.log(typeof(m));//undefined
// 判断变量是否存在
if(typeof a != 'undefined'){
//变量存在
}

使用instanceof判断引用类型

function Student(){
    console.log('this is student');
}

var stu = new Student();
console.log(stu instanceof Student); // true
console.log(stu instanceof Object); // true

判断是否为数组

let arr = [1,2,3,4,5];
console.log(arr instanceof Array); // true
console.log(typeof arr); // 只能判断类型为 "Object"
2.js中的原型链理解

参考:

原型概述:

任何对象都有一个原型对象,这个原型对象由对象的内置属性**proto**指向它的构造函数的prototype指向的对象,即任何对象都是由一个构造函数创建的,被创建的对象都可以获得构造函数的prototype属性,注意:对象是没有prototype属性,只有方法才有prototype属性。

任何对象都有一个constructor属性,指向创建此对象的构造函数,比如说{}对象,它的构造函数是function Object(){}

理解原型的要点:

  • 只有对象才会有__proto__属性,这个属性是个对象类型
  • 只用函数才会有prototype属性,这个属性也是个对象类型
  • 对象的__proto__的属性指向它的构造函数的prototype属性

下面根据代码来理解构造函数与原型对象之间的关系

// 1.创建一个构造函数
function Person(name,age) {
    this.name = name;
    this.age = age;
}

// 2.打印这个构造函数上的prototype属性
// 返回一个对象类型,这个就是原型对象
{
    constructor: ƒ Person(name,age)
	__proto__: Object
}

根据上面代码看出,函数的prototype属性是一个对象,这个对象就是原型对象,并且该对象内部有constructor属性,这个属性指向的就是其本身的构造函数。

原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问到这个原型对象,我们可以将共有的内容,统一设置到原型对象中。

在这里插入图片描述

看图一句话理解:构造函数的prototype属性指向它的原型对象,而原型对象的constructor属性又指向构造函数本身

根据构造函数创建一个对象

// 1.使用构造函数创建一个对象
var p = new Person('zs',20);
console.log(p);

// 2.打印这个对象
{
    constructor: ƒ Person(name,age)
	__proto__: Object
}

上面代码可以看出,实例对象上的proto属性所对应的对象就是原型对象

在这里插入图片描述

所以,总结原型的终极就是:构造函数的prototype属性指向它的原型对象,而原型对象的constructor属性又指向构造函数本身。由构造函数创建的实例对象上的proto属性访问的就是原型对象

// 这时候再去理解上面原型概述的话语:这个原型对象由对象的内置属性_proto_指向它的构造函数的prototype指向的对象
// 就很轻松了
console.log(p.__proto__ === Person.prototype); //true

因此,实例对象的proto属性和构造函数的prototype属性是相等的

什么是原型链:

在js中,对象和对象之间也有关系,并不是孤立存在的。对象之间的继承关系,在js中通过prototype对象(即原型对象中的proto属性)指向父类对象,直到指向object对象为止。

每个对象都可以有一个原型_proto_,这个原型还可以有它自己的原型,以此类推,形成一个原型链。查找特定属性的时候,我们先去这个对象里去找,如果没有的话就去它的原型对象里面去,如果还是没有的话再去向原型对象的原型对象里去寻找… 这个操作被委托在整个原型链上,这个就是我们说的原型链了。

看下面的代码:

// 构造函数Person
function Person(name,age) {
    this.name = name;
    this.age = age;
}

// 在原型上定义属性
Person.prototype.a = 123;
Person.prototype.sayHello = function () {
    console.log('hello');
};

let p = new Person();
console.log(p.a);
p.sayHello();

person实例中没有a这个属性,从person对象中找不到a属性就会从person的原型也就是person.proto,也就是Person.prototype中查找,找到a的值为123,假如person._proto_中也没有该属性,又该如何查找?

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层的Object为止。
Object是JS中所有对象数据类型的基类(最顶层的类),在object.Prototype上没有_proto_这个属性

Console.log(Object.prototype.proto===null)//true

在这里插入图片描述

3.浅拷贝和深拷贝

参考:https://juejin.cn/post/6844903745961066503

**浅拷贝概念:**创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象

深拷贝概念:深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响

所以:浅拷贝与深拷贝的最大区别就是:对于对象类型,浅拷贝只会拷贝引用地址,而深拷贝则会将对象内容也完全拷贝

浅拷贝的代码示例:

// 创建一个对象
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}

// 使用Object.assign()方法进行浅拷贝
let b = Object.assign({}, a);
console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";


console.log(a);
// {
// 	name: "change",
// 	book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "55"}
// } 

深拷贝代码示例:

JSON.parse(JSON.stringify(object))

let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";

console.log(a);
// {
// 	name: "change",
// 	book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "45"}
// } 

4.用js实现深拷贝

思路:定义一个函数,然后传入一个obj,获取这个obj上的所有属性,然后遍历,获取对应的value,根据value的不同类型分别进行不同的处理,如果是基本类型直接赋值,如果是数组类型,copy这个数组,如果是对象类型,则需要进一步进行遍历

下面这段深拷贝代码是自己按照上面思路实现的,功能可以实现,但是有一定的代码冗余性,还可以进行优化

// 深拷贝
function deepClone(obj) {
    if(obj == null){
        return null;
    }

    let _thisObj = {};

    let objKeys = Object.keys(obj);

    objKeys.forEach(((value, index) => {

        let _tempVariable = obj[value];

        // 如果是变量
        if(typeof _tempVariable === 'string' || typeof _tempVariable === 'number' ||
           typeof _tempVariable === 'boolean' || typeof _tempVariable === 'undefined'){

            _thisObj[value] = _tempVariable;
        }

        // 如果是数组
        if(_tempVariable instanceof Array){
            let _arr = [];
            value.forEach((val) =>{
                _arr.push(value);
            });
            _thisObj[value] = _arr;
        }

        // 如果是对象
        if(_tempVariable instanceof Object){
            let resObj = deepClone(_tempVariable);
            _thisObj[value] = resObj;
        }

    }));

    return _thisObj;
}
}

进行调用

let Person = {
    name: 'xiaoming',
    age: 20,
    sex: 'nan',
    student: {
        book:{
            yuwen:'语文',
            shuxue:'数学',
            excrise: {
                yuwenScore:100,
                shuxueScore:100
            }
        }
    }
};
let deepClonePerson = deepClone(Person);
console.log("deepClonePerson:",deepClonePerson);

下面参考了其他网友的代码,这个版本代码比较简化,但是思路大致一样

// 深拷贝
function deepClone(o) {
    // 判断如果不是引用类型,直接返回数据即可
    if (typeof o === 'string' || typeof o === 'number' || typeof o === 'boolean' || typeof o === 'undefined') {
        return o
    } else if (Array.isArray(o)) { // 如果是数组,则定义一个新数组,完成复制后返回
        // 注意,这里判断数组不能用typeof,因为typeof Array 返回的是object
        console.log(typeof []);  // --> object
        var _arr = [];
        o.forEach(item => { _arr.push(item) });
        return _arr
    } else if (typeof o === 'object') {
        var _o = {};
        for (let key in o) {
            _o[key] = deepClone(o[key])
        }
        return _o
    }
}
5.js执行机制与代码循环
  • 直接看大佬阮一峰的博客吧:http://www.ruanyifeng.com/blog/2014/10/event-loop.html
6.简单说说js中的内存模型

参考:https://blog.csdn.net/qq_42349946/article/details/108370789

JS 中的数据类型,整体上来说只有两类:基本类型和引用类型。

类型说明
基本类型Sting、Number、Boolean、null、undefined、Symbol。它们被放在 JS 的栈内存里存储
引用类型Object、Array、Function。也叫做复杂数组类型。它们被放在 JS 的堆内存里存储

栈是线性表的一种,而堆则是树形结构

在这里插入图片描述

上图即是js的内存模型

7.简单说说Js中的垃圾回收机制

每隔一段时间,JS 的垃圾收集器就会对变量做 “巡检”。当它判断一个变量不再被需要之后,它就会把这个变量所占用的内存空间给释放掉,这个过程叫做垃圾回收

js中的垃圾回收算法有两种,非常类似java当中的jvm虚拟机回收算法

**引用计数法:**在引用计数法的机制下,内存中的每一个值都会对应一个引用计数(当变量被引用一次时,这个值就会加一)。当垃圾收集器感知到某个值的引用计

数为 0 时,就判断它 “没用” 了,随即这块内存就会被释放。

**标记清除法:**标记清除法是现代浏览器的标准垃圾回收算法。在标记清除算法中,一个变量是否被需要的判断标准,是它是否可抵达,也就是可达性分析这个算法有两个阶段,分别是标记阶段和清除阶段: 标记阶段:垃圾收集器会先找到根对象,在浏览器里,根对象是 Window;在 Node 里,根对象是 Global。从根对象出发,垃圾收集器会扫描所有可以通过根对象触及的变量,这些对象会被标记为 “可抵达”。清除阶段: 没有被标记为 “可抵达” 的变量,就会被认为是不需要的变量,这波变量会被清除。

8.内存泄漏

参考:https://blog.csdn.net/qq_40028324/article/details/92970588

**什么是内存泄漏:**不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)

内存泄漏的案例:

  • 意外的全局变量

    function foo() {
        bar1 = 'some text'; // 没有声明变量 实际上是全局变量 => window.bar1
        this.bar2 = 'some text' // 全局变量 => window.bar2
    }
    foo();
    // 在这个例子中,意外的创建了两个全局变量 bar1 和 bar2
    
  • 被遗忘的定时器和回调函数

    var serverData = loadData();
    setInterval(function() {
        var renderer = document.getElementById('renderer');
        if(renderer) {
            renderer.innerHTML = JSON.stringify(serverData);
        }
    }, 5000); // 每 5 秒调用一次
    

    如果后续 renderer 元素被移除,整个定时器实际上没有任何作用。但如果你没有回收定时器,整个定时器依然有效, 不但定时器无法被内存回收, 定时器函数中的依赖也无法回收。在这个案例中的 serverData 也无法被回收。

  • 闭包

    var theThing = null;
    var replaceThing = function () {
      var originalThing = theThing;
      var unused = function () {
        if (originalThing) // 对于 'originalThing'的引用
          console.log("hi");
      };
      theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
          console.log("message");
        }
      };
    };
    setInterval(replaceThing, 1000);
    

    这段代码,每次调用 replaceThing 时,theThing 获得了包含一个巨大的数组和一个对于新闭包 someMethod 的对象。同时 unused 是一个引用了 originalThing 的闭包。

    这个范例的关键在于,闭包之间是共享作用域的,尽管 unused 可能一直没有被调用,但是 someMethod 可能会被调用,就会导致无法对其内存进行回收。当这段代码被反复执行时,内存会持续增长。

  • DOM操作

    很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 对象 中

    var elements = {
        image: document.getElementById('image')
    };
    function doStuff() {
        elements.image.src = 'http://example.com/image_name.png';
    }
    function removeImage() {
        document.body.removeChild(document.getElementById('image'));
        // 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.
    }
    

    上述案例中,即使我们对于 image 元素进行了移除,但是仍然有对 image 元素的引用,依然无法对齐进行内存回收

9.JS中常见的错误有哪些
错误说明
Uncaught SytanxError语法错误,很显然要么多输入字符,要么就是少括号之类的
Uncaught ReferenceError引用类型错误,引用不存在的属性就会报错
Uncaught TypeError提供的类型错误,提供的类型不是Js代码中所需要的。比如常见的调用一个不存在的函数就会报: xx is not a function
RangeError范围错误,比如创建数组给个负值
URIError调用URL函数时传参不正确导致的错误
10.js中跳转页面的方法有哪些,他们的区别是什么

有三种方法:location.href,location.replace,location.reload

  • location.href:用法location.href="http://www.baidu.com",会写入 浏览器的历史 window.history 对象中 ,这就导致可以通过浏览器上的后退按钮来返回原页面
  • location.replace:用法location.replace("http://www.baidu.com"),不会写入浏览器历史的history对象中,所以没法点击后退按钮
  • location.reload():就相当于浏览器上的刷新按钮,可传入俩参数,为false时表示从本地缓存加载页面,为true时表示重新从服务器获取页面
11.如何在JS中动态添加/删除对象的属性
  • 使用object.property_name = value向对象添加属性
  • delete object.property_name 用于删除属性
12.JS中的substr()和substring()函数有什么区别
let a = 'helloworld';
console.log(a.substr(1, 4)); // ello
console.log(a.substring(1,4));// ell

substr:从下标位置开始,截取4个元素,左开右开[1,4]

substring:从下标位置开始,截取4-1个元素,左开右闭[1,4)

13.一道经典的代码题:函数提升,运算符优先级,变量污染等

参考: https://www.cnblogs.com/haojf/p/13037941.html

function Foo() {
    getName = function () { alert (1); };
    return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}

//请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

这道题的经典之处在于它综合考察了面试者的JavaScript的综合能力,包含了变量定义提升、this指针指向、运算符优先级、原型、继承、全局变量污染、对象属性及原型属性优先级等知识

运行结果是:2 4 1 1 2 3 3

我们先来分析以下这段代码的做了什么事

// 1.定义了一个Foo构造函数
function Foo() {
    getName = function () { alert (1); };
    return this;
}
// 2.给Foo对象添加了一个静态方法getName
Foo.getName = function () { alert (2);};
// 3.为Foo的原型对象新创建了一个叫getName的匿名函数
Foo.prototype.getName = function () { alert (3);};
// 4.通过函数变量表达式创建了一个getName的函数
var getName = function () { alert (4);};
// 5.声明了一个普通函数getName
function getName() { alert (5);}

再来看解析

// 1.这里调用的Foo对象上的静态方法,无需实例化对象即可调用,所以输出的是2
Foo.getName(); // 2

// 2.这里看似是调用了函数getName输出的是5,实际上这是一个坑,没错,声明式函数getName会被提升到前面,但是后面又用函数表达式声明了一个同样名为
// getName的函数,这时候后面的这个同名函数会覆盖掉前面的函数,所以输出4
getName(); // 4

// 3. 这里先执行函数Foo(),然后给全局变量getName赋值,所以var getName = function(){ alert (1); }; 然后Foo函数返回的是一个window对象
// 相当于window.getName();所以输出1
Foo().getName();// 1

// 4.由于上面的Foo().getName()把函数修改了,所以这里输出的是1,因为直接调用getName函数,相当于window.getName()
getName();// 1

// 5.这里涉及到运算符优先级的问题,new(带参数列表)> 函数调用 > new(无参数列表)
// 所以这段代码相当于new (Foo.getName()); 因此返回2
new Foo.getName(); // 2

// 6.这里依然根据运算符优先级,所以这段代码是这样的 (new Foo()).getName();
// 由于new出来的对象是个空对象,自然没有getName()这个函数,所以回去原型上去找,所以输出3
new Foo().getName();

// 7.根据5点和第6点说明分析,这里等价于new (new Foo().getName()),所以输出3
new new Foo().getName();

关于第二点的详细解析:

var getName = function () { alert (4);};
function getName() { alert (5);}

这里的代码在被Js解析后是这样的

var getName; // 此时这里为undefined
function getName() { alert (5);}
getName = function () { alert (4);}; // getName = function ()实际上相当于 function getName();

所以js在执行到最后一段代码时,发现getName后面值是函数表达式,所以会覆盖掉上面的getName()函数声明,所以这里输出的是4

关于第三点的详细解析:

下面这段代码由于变量的提升,var getName会提升到作用域最前面

var getName = function () { alert (4);}; 

所以解析后的代码是这样的

var getName;
function Foo() {
    getName = function () { alert (1); };
    return this;
}
getName = function () { alert (4);}; 

Foo().getName();

先执行函数Foo,这个函数内部把全局变量getName的值给修改成了getName = function () { alert (1); };,而这个函数返回的this对象是window对象,所以。Foo().getName()相当于window.getName();所以输出的值就是1了

关于最后三点的分析:

实际上最后三点比较难,考查的是运算符优先级的问题,可以去搜以下MDN上关于运算符优先级表,上面列出了运算符优先级。

无参数列表的优先级为18,而成员访问的优先级为19,高于无参数列表。因此new Foo.getName()先执行Foo.getName()

带参数列表的优先级为19,而成员访问的优先级也为19,按照运算符规则(同一优先级,按照从左向右的执行顺序),new Foo().getName()先执行new Foo(),再对new之后的实例进行成员访问.getName()操作。

所以总结出了new(带参数列表)> 函数调用 > new(无参数列表)

另外new Foo()创造出来的对象是个空对象,自然会去原型上找getName方法,根据Object._proto_ = constructor.Prototype。所以能找到原型上的方法

14.this的指向问题

参考:https://www.cnblogs.com/pssp/p/5216085.html

首先必须要说的是,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁实际上this的最终指向的是那个调用它的对象(这句话有些问题,后面会解释为什么会有问题,虽然网上大部分的文章都是这样说的,虽然在很多情况下那样去理解不会出什么问题,但是实际上那样理解是不准确的,所以在你理解this的时候会有种琢磨不透的感觉

上面这段话说的非常有道理,不能单纯的认为this指向是它的调用者,虽然大多数情况下是。还是要根据它所在的上下文去理解

下面根据代码的环境上下文可以做一个总结:

  • 普通函数的this指向window,注意是单独定义的函数,不是定义在对象中的

    function func() {
        console.log(this); // window
    }
    
    let fn = function () {
        console.log(this); // window
    }
    
  • 定义在对象中的函数,很明显,对象调用,this指向这个对象

    var o = {
        user:"追梦子",
        fn:function(){
            console.log(this.user);  //追梦子
        }
    }
    o.fn();
    

    这里的this指向的是对象o,因为你调用这个fn是通过o.fn()执行的,那自然指向就是对象o,这里再次强调一点,this的指向在函数创建的时候是决定不了的,在调用的时候才能决定,谁调用的就指向谁,一定要搞清楚这个。

    再看一个例子

    var o = {
        a:10,
        b:{
            a:12,
            fn:function(){
                console.log(this); // b
            }
        }
    }
    o.b.fn();
    

    尽管是对象嵌套调用,但是this指向的仍然是它的调用者

因此:this的指向问题需要分情况讨论,这下面的三种情况说的是普通函数

情况1:如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window,这里需要说明的是在js的严格版中this指向的不是window, 但是我们这里不探讨严格版的问题,你想了解可以自行上网查找。

情况2:如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。

情况3:如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象

下面再看一个特殊的例子:

let obj={
    a:222,
    fn:function(){    
        setTimeout(function(){console.log(this)}) // window
    }
};
obj.fn();//undefined
let obj={
    a:222,
    fn:function(){    
        setTimeout(()=>{console.log(this)});// obj
    }
};
obj.fn();//222

分析:第一段代码传入的是个普通函数,这个setTimeout里的参数函数其实是由setTimeout本身调用的,setTimeout是window对象下的,所以这里的this指向window。第二段代码由于是箭头函数,箭头函数没有this对象,它的this从上一级继承,也就是obj对象

下面再看一个怪异例子

var x=11;
var obj={
 x:22,
 say:()=>{
   console.log(this.x);
   console.log(this); // window
 }
}
obj.say();
//输出的值为11

看完这段代码,我靠,你会发现上面说的情况又不符合了。这里的this不是继承它的上一级obj。注意啊,这里箭头函数本身与say平级以key:value的形式,也就是箭头函数本身所在的对象为obj,而obj的父执行上下文就是window,也就是说,say箭头函数的上一级就是obj的上一级,就是windows对象!因此这里的this.x实际上表示的是window.x,因此输出的是11。所以这里涉及到的关键点在于箭头函数执行的上下文。

类似的还有:

var a=11;
function test1(){
    this.a=22;
    let b=function(){
        console.log(this);// window
    };
    b();
}

test1();
function test2(){
    this.a=22;
    let b=()=>{console.log(this)};// window
    b();
}

test2();

所以,关于this的总结就是:

  • 单独定义的函数(不在对象,其他函数中定义的)一般this指向window对象

    例如

    function func() {
        console.log(this); // window
    }
    
    let fn = function () {
        console.log(this); // window
    }
    
    let b = ()=>{ console.log(this) };
    
  • 定义在对象中的普通函数,一般this指向调用它的对象

  • 箭头函数根据不同的情况,不仅跟它的调用者有关,也跟它执行的上下文有关。箭头函数的this指向箭头函数定义时所处的对象,而不是箭头函数使用时所在的对象,默认使用父级的this

15.函数的节流与防抖

参考:https://blog.csdn.net/zuorishu/article/details/93630578

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

函数节流(throttle):高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率。

函数节流(throttle)与函数防抖(debounce)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值