JS基础知识面试题(1)

慕课 快速搞定前端技术一面 第四五六章 笔记总结

JS变量类型和计算

JS中使用typeof能得到的哪些类型

  • 6种,number、undefined、string、boolean、Symbol、function、object
  • 准确判断5种 值类型(值类型中不能判断null) ,但是 引用类型 中只能判断函数,其余的 数组、对象、正则、日期 都被识别为object
  • 注意:值类型中null(空指针)也被识别为object!
  • 想要区分引用类型可使用instanceof运算符
  • 基本(原始)、复杂与全局数据类型

何时使用===何时使用==

  • 注意:不要混淆,只有NaN和自己不相等,null和undefined都和自己是相等的。
  • 【需要值为null或者undefined时使用==简写,因为null==undefined
    只有一种情况使用双等号==,其他时候全部使用三等号===
    何时使用`==`此时第三行代码相当于第四行代码的
    简写
    ,这是jquery的源码推荐写法
  • 双等号==值相等,而三等号===严格相等(值及类型是否完全相等),双等号==在运算的时候会进行 类型转换,而三等号===则不会。
    1. 对于string,number等基础类型=====是有区别的
      1. 不同类型间比较==会先“转化成同一类型后的值”看“值”是否相等,===如果类型不同,其结果就是不等
        • ==统一先转换为数字比较(其中Boolean只有两个值:true==1,false==0;null与undefined相等;字符串数字等于数字值,空字符串""==0
      2. 同类型比较,直接进行“值”比较,两者结果一样
    2. 对于Array,Object等引用类型=====是没有区别的,都是进行“指针地址”比较
    3. 基础类型与高级类型之间进行比较,=====是有区别的
      1. 对于==将引用类型转化为基础类型,进行“值”比较
      2. 因为类型不同,===结果为false

JS中有哪些内置函数

内置函数(数据封装类对象):
ObjectArrayBooleanStringFunctionDateRegExpErrorNumber


JS变量按存储方式分为哪些类型,有何特点

  • 按照存储方式区分为:值类型、引用类型
    • 值类型(基本类型):名字和值都会储存在栈内存中。
    • 引用类型名字存在 栈内存 中,值存在 堆内存 中,但是 栈内存 会提供一个 引用的地址 指向堆内存中的值(指针)。引用类型
  • 特点:值类型相当于复制,引用类型则是 指针复制 并不是真正的拷贝,两个变量会共用同一个属性,他们的值的修改是相互干预的。
    按存储方式区分JS变量

如何理解JSON

  • JSON是一种数据格式但从JS来看JSON就是一个JS内置对象,他有两个方法,“JavaScript Object Notation” 即 “JavaScript 对象表示法”,它是存储和交换文本信息的语法
    JSON
    stringify()把对象变成字符串
    parse()把字符串变成对象

手写深拷贝

  • 注意判断值类型和引用类型
  • 注意判断是数组还是对象(判断对象自身属性)
  • 递归
  • 不使用for…of的原因: 数组元素/对象属性为不可迭代的对象时会报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 深拷贝方法1
* @param {Object} obj 要拷贝的对象
*/
function deepClone(obj) {
// 如遇值类型则直接返回,否则继续执行
if (typeof obj !== "object" || obj === null) {
return obj;
}

// 初始化返回结果
let cloneObj = Array.isArray(obj) ? [] : {};

// 遍历对象属性/数组元素
for (key in obj) {
// 筛选对象自身属性
if (obj.hasOwnProperty(key)) {
// 递归调用deepClone(),
cloneObj[key] = deepClone(obj[key])
}
}

// 返回结果
return cloneObj;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 深拷贝方法2
* @param {Object} obj 要拷贝的对象
*/
function deepClone(obj){
// 根据传入的参数obj判断拷贝数组还是对象
let objClone = Array.isArray(obj)?[]:{};
// 判断参数obj是否是引用类型(数组/对象)
if(obj && typeof obj==="object"){
// 是对象则用for in遍历 对象属性
for(key in obj){
// 如果是非继承属性
if(obj.hasOwnProperty(key)){
//判断ojb子元素(数组元素/对象属性)是否为引用类型(数组、对象、函数)
if(obj[key]&&typeof obj[key] ==="object"){
//是则递归复制
objClone[key] = deepClone(obj[key]);
}else{
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
let a=[0, 1, null, [1, 2], { name: "a" }, function a() { return 1; }],
b=deepClone(a);
b[3][0] = 5;
console.log(a,b);

原型和原型链

如何准确判断变量是不是数组(typeof不行)

  1. instanceof判断引用类型(对象、数组、函数)属于哪个构造函数(Object()、Array()、Funtion())答案
    • 注意:数组通过instanceof到Array或Object都返回true
  2. Array.isArray(obj)

class的原型本质

  • class实际上是函数(typeof)的语法糖
  • 原型规则
    1. 引用类型可自由扩展属性
    2. 引用类型的__proto__属性(隐式原型),属性值是一个普通的对象
    3. 函数有prototype属性(显式原型),属性值也是一个普通的对象
    4. 引用类型(数组、对象、函数)的__proto__属性值 都指向它的 构造函数的prototype属性值
    5. 当试图得到一个对象的某个属性/方法时,如果这个对象本身没有这个属性,那么会去它的__proto__属性(即它的构造函数的prototype属性)中寻找
  • class与原型规则
    1. 每个class(本质是函数)都有显式原型prototype
    2. 每个实例都有隐式原型__proto__
    3. 实例的__proto__指向对应class的prototype
    4. 子类的显示原型(可理解为一个对象)的隐式原型 指向 父类的显式原型
    5. 父类的显示原型(可理解为一个对象)的隐式原型 指向 Object(构造函数 )的显式原型

手写一个简易的jQuery,考虑插件和扩展性

  • $(selector).get(index)通过检索匹配jQuery对象得到对应的DOM元素
  • $(selector).each(function(index,element))遍历一个jQuery对象,为每个匹配元素执行一个函数。
  • 事件绑定on()在选定的元素上绑定一个或多个事件处理函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    class jQuery {
    constructor(selector) {
    // 使this为DOM元素组成的类数组
    const res = document.querySelectorAll(selector);
    const length = res.length;
    // 遍历类数组对象,放入this中
    for (let i = 0; i < length; i++) {
    this[i] = res[i];
    }
    this.length = length;
    this.selector = selector;
    }
    // .get(index)匹配jQuery对象得到对应的DOM元素
    get(index) {
    return this[index];
    }
    // each() 方法遍历一个jQuery对象,为每个匹配元素执行一个函数
    each(fn) {
    for (let i = 0; i < this.length; i++) {
    let elem = this[i];
    fn(elem);
    }
    }
    // on()在选定的元素上绑定一个或多个事件处理函数。
    on(type, fn) {
    this.each(elem => {
    elem.addEventListener(type, fn, false)
    })
    }
    // 扩展很多 DOM API...
    }

    // 插件(做一个弹出提示框的插件)
    jQuery.prototype.dialog = function (info) {
    alert(info);
    }

    // “造轮子”
    class myJQuery extends jQuery {
    constructor(selector) {
    super(selector)
    }
    // 扩展自己的方法
    addClass(className) {

    }
    style(data) {

    }
    }

    // let $p=new jQuery("p");
    // $p.get(1)
    // $p.each((elem) => console.log(elem.nodeName))
    // $p.on('click', () => alert('clicked'))
    // $p.dialog("abc")

写一个原型链继承的例子

重点:子构造函数.prototype=new 父构造函数;

基础例子
基础例子
1-12:创建两个构造函数Animal()、Dog():
13:将Dog的显示类型prototype更改为Animal()的一个对象:
15:new一个Dog()的对象hashiqi,此时hashiqi不仅有Dog()的属性bark,还有一个继承自Dog.prototype(即Animal()的一个对象)的eat属性
【eat属性 就来自原型链继承。】

封装DOM查询的例子

  • 功能:获取DOM节点的内容并给节点绑定一个事件。
  • 原理:通过扩展原型的方式来:根据id获取一个DOM节点,扩展Elem的显式原型方法html(),调用html()可改变/返回节点的innerHTML再扩展一个原型方法on(),调用on()可绑定一个事件函数。(调用html()或者on()时都是到对象的隐式原型(即构造函数的显式原型)中找到的,这就是原型链继承
  • 测试方法:先通过不传参的html()测试是否能成功获取该id的DOM节点的innerHTML,可以则传参改变该innerHTML,并使用on()绑定click函数用于点击节点后弹出警示框。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    //根据传入的id获取DOM元素elem
    function Elem(id) {
    //获取DOM元素并赋予elem
    this.elem = document.getElementById(id);
    }

    // 修改Elem的显式原型,增加html(),使得调用html()可改/返innerHTML
    Elem.prototype.html = function (val) {
    var elem = this.elem;//绑定elem
    //如果有传参就用参数替换掉获取到的DOM元素的innerHTML
    if (val) {
    elem.innerHTML = val;
    // 可不反回this,返回是为了实现链式操作
    return this;
    } else {
    return elem.innerHTML;
    }
    }

    // 同上增on(),使调用on()可绑定事件函数,type是函数名,fn是函数
    Elem.prototype.on = function (type, fn) {
    var elem = this.elem;
    elem.addEventListener(type, fn);
    }

    var div1 = new Elem("main-navigation");
    // console.log(div1.html());说明能够获取innerHTML
    div1.html("<p>你好呀</p>");
    div1.on("click", function () {
    alert("你点击了我!") ;
    })

【关于“addEventListener”可参考笔记DOM基础的添加事件句柄。要区别于xx.onclick=function(){函数名(参数);}

测试:打开一个网页,找到菜单栏对应的id
测试1
此id用作参数调用构造函数Elem()创建一个对象div1,可打印出菜单栏的innerHTML,说明可以成功获取到div1的innerHTML
测试2
调用div1的显式原型中的html()进行数据的替换,调用div1的显式原型中的on()进行事件函数的绑定,点击div1后弹出警示框:
测试3

补充:链式操作

因为我们的html()返回的是this,相当于返回了div1,所以可以链式操作:

1
2
3
div1.html("<p>你好呀</p>").on("click", function () {
alert("你点击了我!");
})

描述new一个对象的过程

【考察 构造函数】

答案

  1. 创建一个新(空)对象f
  2. 将新对象f的隐式原型(__proto__)指向构造函数的显式原型(prototype
  3. 构造函数Foo()的this指向这个新对象f(这里的this是个空对象)
  4. 让新对象f去执行Foo()函数代码(即对this进行赋值)
  5. 如构造函数无返回值或返回一个非对象值,则返回新对象f;如返回值是新对象f则直接返回该对象。(屏蔽第5行代码也是一样的效果,默认返回,此时this已经不是空对象了)

作用域和闭包

变量提升的理解

【变量定义、函数声明(注意和函数表达式的区别)】

  • 首先要明确,变量是声明被提升,使用函数声明方式声明的函数在ES5中是整个函数被提升,在ES6中只提升函数的声明。使用函数表达式声明的函数不存在函数提升,和ES6一样只提升函数的声明(具体见例子),函数内容(变量初始化的值)是不会被提升的,他们将被留在原来的位置。
  • 这导致了JavaScript 函数能够在声明之前被调用,不过会undefined,还是建议规范书写代码。
  • ES6之前只有全局作用域和函数作用域,这两个作用域都存在变量提升
  • var命令声明的变量,不管在什么位置,变量声明都会被提升到当前作用域的头部。(变量的提升只会对var命令声明的变量有效,其他不是用var命令声明的变量,不会发生变量的提升。)
  • 注意:变量的提升只存在全局/函数作用域中,**if(){}for(){}都不是函数作用域,他们里面的变量会提升到包裹他们的全局/函数作用域的顶部**。
  • ES6中取代varlet命令的作用域限定在块级,使用let声明的变量不存在变量提升
  • 函数和变量相比,会被优先提升。这意味着函数会被提升到更靠前的位置。
  • 补充:当出现多个同名变量与同名函数时,调用该变量名时的优先级为:变量声明< 函数声明 < 变量赋值(具体参考牛客网20191215第7题)
  • 通过函数声明方式创建的函数会被提升,通过表达式方法创建的函数不会被函数提升(他会类似于变量提升):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    console.log(a)  // f a() { console.log(a) }
    console.log(b) //undefined

    // 函数声明,整个函数提升
    function a() {
    console.log(a)
    }

    // 函数表达式,只提升b的声明,即var b=undefined
    var b = function(){
    console.log(b)
    }

this的不同使用场景

  1. 普通函数中执行,**this指向window**
    • 例子window.fn1();fn1()效果一样,所以函数fn1()是由window对象调用的,根据谁调用函数的this就指向谁的原则,this指向window
  2. 借助call()/apply()/bind()绑定/改变this指向,绑定的this是谁就是谁
    • 注意:与call、apply不同,必须要使用**函数表达式创建才能使用bind()**,使用bind()传参的方式也不同,可参考笔记bind()bind
  3. 对象属性(方法)中 执行,this指向对象本身
  4. class/构造函数 里使用thisthis指向class实例/构造函数实例本身
  5. 箭头函数 里使用thisthis是该函数的上级作用域的this
    例子1

手写bind函数

  • 类数组转换为数组的多种方法
  • 剩余参数...是做聚合的,它会将那些没有对应形参的实参们聚合成一个数组。而扩展运算符...是做展开的,符号都是...,但含义不同。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // fn.bind(this,arg1,arg2)()
    // 函数的方法,所以写在Fuction原型对象上
    Function.prototype.myBind = function (context = window) { // ES6方法,为参数添加默认值
    // 传入的参数1 context 设置为this,即现在调用方法的对象
    let obj = context
    // this为f.myBind(...) 中的 f
    obj.fn = this
    let args = [...arguments].slice(1);
    // 和call、apply不同的是bind返回的是一个新的函数
    return function () {
    return obj.fn( ...args);
    }
    }

    // 用法:f.myBind(obj,arg1)()
    function f(a, b) {
    console.log(a + b)
    console.log(this.name)
    }
    let obj = {
    name: "李四"
    }
    f.myBind(obj, 1, 2)() // 3 李四

创建10个a标签,点击时弹出对应的序号

错误的例子

错误的例子
错误原因分析

  1. 确实可以得到0-9的10个<a>标签
  2. 但是**addEventListener()中添加的函数时要点击以后才会执行的,当我们10个标签都出现在页面上以后,i值变为10**。
  3. for循环这10下是个很快的过程,等循环结束我们点击a标签,触发click的函数时,i是个自由变量,他要去父级作用域中找i的值,此时i已经循环到了10
  4. 所以不管点击哪个标签,弹出的警示框的i都是10,并不是对应的序号

总结:之所以会错就是因为这里的i是 全局作用域 下的i。

理解方式2:
1.循环是同步的,所以创建标签时得到0-9是没问题的。
2.但 事件绑定是异步的,所以他会等循环全部结束以后才开始执行
3.可i是全局变量,也就是说每次循环改变的都是同一个i的值,循环结束后i值变为10,此时事件绑定函数才开始执行,那么他每次到父级作用域中取到的其实都是**i=10,所以不管点击哪个标签弹出的都是10**。

正确示例

  1. 方法1:【使用闭包的持久性,注意重点不在匿名自执行函数,将i传进函数中作为 函数作用域 的变量】:正确示例
    1. i=0时生成一个函数,i=1时又生成另外一个函数,总共通过循环创建了10个函数
    2. 那么第9行代码去获取i的值时就会到当前作用域(函数作用域)中找,自然也不会10个标签都找到同一个i了。
  2. 方法2:【for中使用let代替var定义i】
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //  let 是块级作用域,所以 let代替var定义i 能起到和闭包相同的效果,每一次循环就产生一个块级作用域
    // 注意:let i在for外面没用,那样i的块级作用域太大了(全局作用域)
    var a;
    for (let i = 0; i < 10; i++) {
    a = document.createElement("a");
    a.innerHTML = i + "<br/>";
    a.addEventListener("click", function (e) {
    e.preventDefault();
    alert(i);
    })
    document.body.appendChild(a);
    }

如何理解作用域

  • 自由变量,即当前作用域还没有定义的变量
  • 父级作用域:在函数定义时规定的,不需要管执行顺序。
    • 补充:this指向时函数执行时确定的,箭头函数没有自己的this,它则指向自己的父级作用域的this。
  • 作用域链:即自由变量的层层查找,函数中找不到 自由变量 的值时一层层向上面的 父级作用域 进行查找的链式流程。
  • 闭包的使用场景(也和作用域有关):
    • 本质就是在一个函数内部定义另一个函数
    • 作用:让一个函数有权访问另一个函数作用域中变量。(即让函数访问自己的父级作用域这个函数中的变量

实际开发中闭包的应用

隐藏数据,如做一个简单的cache工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 闭包隐藏数据,只提供 API
function createCache() {
const data = {} // 闭包中的数据,被隐藏,不被外界访问
return {
set: function (key, val) {
data[key] = val
},
get: function (key) {
return data[key]
}
}
}

const c = createCache()
c.set('a', 100)
console.log( c.get('a') )

判断用户是不是第一次校验
实际应用1
好处:将存储用户id的 变量_list 封装起来,那么闭包外面就拿不到 变量_list,减少用户数据泄露的危险。

游戏中的作用
例子2
假设我们在做一个游戏,在写其中关于「还剩几条命」的代码。如果不用闭包,你可以直接用一个全局变量:window.lives = 30 // 还有三十条命
但这样看起来很不妥。万一不小心把这个值改成 -1 了怎么办。所以我们不能让别人「直接访问」这个变量。怎么办呢?
用局部变量
但是用局部变量别人又访问不到,怎么办呢?
**暴露一个访问器(函数),让别人可以「间接访问」生命值lives**。
使用闭包后,在其他的 JS 文件中就可以使用 window.奖励一条命() 来涨命,使用 window.死一条命() 来让角色掉一条命。


,