秋招面试准备 JS 2

1JS判断类型如何判断JS类型 - 知乎 (zhihu.com)

一JS的类型

JS的基本类型共有七种:bigInt(bigInt是一种内置对象,是处symbol外的第二个内置类型)、number、string、boolen、symbol、undefined、null。复杂数据类型有对象(object)包括基本的对象、函数(Function)、数组(Array)和内置对象(Data等)。

二判断JS的类型

方法一、typeof方法

基本数据类型除了null外都返回对应类型的字符串

typeof 1 // "number" 
typeof 'a'  // "string"
typeof true  // "boolean"
typeof undefined // "undefined"
typeof Symbol() // "symbol"
typeof 42n // "bigint"

注:判断一个变量是否被声明可以用(typeof 变量 === “undefined”)来判断

null返回“object”

typeof null  // "object"

 因为历史遗留的原因,typeof null尝试返回为null失败了,所以要记住,typeof null返回的是object

特殊值NaN返回的是“Number”

typeof NaN // "number"

而复杂数据类型里,除了函数返回了function,其他均返回object 

typeof({a:1}) // "object" 普通对象直接返回“object”
typeof [1,3] // 数组返回"object"
typeof(new Date) // 内置对象 "object"

函数返回function 

typeof function(){} // "function"

 所以我们可以发现,typeof可以判断基本数据类型,但是难以判断除了函数以外的复杂数据类型。于是我们可以使用第二种方法,通常用来判断复杂数据类型,也可以用来判断基本数据类型。

方法二、object.property.toString.call方法,他返回“[object,类型]”,注意返回的格式及大小写,前面是小写,后面是首字母大写

基本数据类型都返回相应的类型

Object.prototype.toString.call(999) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(Symbol()) // "[object Symbol]"
Object.prototype.toString.call(42n) // "[object BigInt]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(true) // "[object Boolean]

复杂数据类型也能返回相应的类型

Object.prototype.toString.call({a:1}) // "[object Object]"
Object.prototype.toString.call([1,2]) // "[object Array]"
Object.prototype.toString.call(new Date) // "[object Date]"
Object.prototype.toString.call(function(){}) // "[object Function]"

这个方法可以返回内置类型

方法三、obj instanceof Object,可以左边放你要判断的内容,右边放类型来进行JS类型判断,只能用来判断复杂数据类型,因为instanceof是用于检测构造函数(右边)的prototype属性是否出现在某个实例对象(左边)的原型链上

[1,2] instanceof Array  // true
(function(){}) instanceof Function // true
({a:1}) instanceof Object // true
(new Date) instanceof Date // true

obj instanceof Object方法也可以判断内置对象。

缺点:在不同window或者iframe间,不能使用instanceof。

其他方法:除了以上三种方法,还有constructor方法 和 duck type方法,具体在文章就不介绍了,个人觉得吃透分清上面三种方法已经足够了。

2数组常用方法JavaScript中数组的常用方法(含ES6) - 简书 (jianshu.com)

1、concat(),不改变原数组

arr1.concat(arr2)连接两个或多个数组,返回一个新的数组

const arr1 = [1, 2, 3]
const arr2 = [4, 5]
const newArr = arr1.concat(arr2)
console.log(newArr) // [1, 2, 3, 4, 5]

2.join(),不改变原数组

join(str)数组转字符串,方法只接受一个参数,默认逗号为分隔符

conat arr = [1, 2, 3]
console.log(arr) // [1, 2, 3]
console.log(arr.join()) // 1,2,3
console.log(arr.join(:)) // 1:2:3

tips:join()实现重复字符串

const str = new Array(4).join('啦')
console.log(str) // 啦啦啦

3.push()&unshift()添加元素操作,改变了原数组

push()向数组的末尾添加一个或多个元素,并返回新的长度

const arr = [1, 2]
console.log(arr.push(3)) // 3
console.log(arr) // [1, 2, 3]

unshift()向数组的开头添加一个或多个元素,并返回新的长度

const arr = [1, 2]
console.log(arr.unshift(3)) // 3
console.log(arr) // [3, 1, 2]

4.shift()&pop()删除元素操作,改变了原数组

shift()删除并返回数组的第一个元素

const arr = [1, 2, 3]
const deleteItem = arr.shift()
console.log(arr) // [2, 3]
console.log(deleteItem) // 1

pop()删除并返回数组最后一个元素

const arr = [1, 2, 3]
const deleteItem = arr.pop()
console.log(arr) // [1, 2]
console.log(deleteItem) // 3

5sort()数组排序,改变原数组

sort()对数组的元素进行排序

const arr = [2, 4, 3, 1]
console.log(arr.sort()) // [1, 2, 3, 4]
console.log(arr) // [1, 2, 3, 4]
  • tips: sort() 不按照数组元素数值的大小对数字进行排序,是按照字符编码的顺序进行排序,那怎么样根据元素数值大小进行排序呢?
const arr = [2, 4, 3, 1]
const arr1 = [...arr].sort((a, b) => a - b)
const arr2 = [...arr].sort((a, b) => b - a)
console.log(arr1) // [1, 2, 3, 4]
console.log(arr2) // [4, 3, 2, 1]

6reverse()反转数组,改变原数组

reverse()颠倒数组中元素的顺序

const arr = [2, 4, 3, 1]
console.log(arr.reverse()) // [1, 3, 4, 2]
console.log(arr) // [1, 3, 4, 2]

7slice()截取数组,不改变原数组

arr.slice(start, end) 从start处开始选取(不包括该元素),从end处结束选取,如果为空的话,那么选取从start到数组结束的所有元素。负数代表方向,从数组尾部开始计算位置

const arr = [1, 2, 3, 4, 6]
console.log(arr.slice(1)) // [2, 3, 4, 6]
console.log(arr.slice(1, -2)) // [2, 3]
console.log(arr.slice(-3, 1)) // [2]
console.log(arr) // [1, 2, 3, 4, 6]

8splice()更新数组,改变原数组

arr.splice(index, howmany, item1, ..., itemX) 向/从数组中添加/删除项目,然后返回被删除的项目,返回含有被删除的元素的数组,若没有删除元素则返回一个空数组。

  • [index] 必传项,整数,规定添加/删除项目的位置,负数表示从数组结尾处规定位置
  • [howmany] 必传项,要删除的项目数量。如果设置为 0,则不会删除项目
  • [item1, ..., itemX] 可选。向数组添加的新项目。
const arr = [1, 2, 3]
const arr1 = arr.splice(2, 1)
console.log(arr1) // [3]
console.log(arr) // [1, 2]

const arr2 = arr.splice(1, 0, 'ss')
console.log(arr2) // ['ss']
console.log(arr) // [1,'ss', 2]

9indexOf()&lastIndexOf()索引方法,不改变原数组

两个方法都是返回要查找的项所在数组中首次出现的位置,没找到的话就返回-1

arr.indexOf(item, start) 从数组的开头开始向后寻找。arr.lastIndexOf(item, start) 从数组的末尾开始向前查找。

  • [item] 必须项,查找的元素
  • [start] 可选,在数组中开始检索的位置,默认0
const arr = [2, 4, 1, 9, 1, 2]
console.log(arr.indexOf(2)) // 0
console.log(arr.lastIndexOf(1)) // 1
console.log(arr.indexOf(3)) // -1

10. find() & findIndex() 根据函数内的判断返回找到的数组内的第一个元素。不改变原数组。 (es6新增方法)

  • 对于空数组不执行
  • [currentValue] 必须项,当前元素
  • [index] 可选。当前元素的索引值
  • [arr] 可选。当前元素所属的数组对象

arr.find((currentValue, index, arr) => {}) 返回通过测试函数内判断的数组的第一个元素的值。当数组中的元素在测试函数中返回true时,返回符合条件的元素,之后不再调用测试函数判断剩下的元素,如果每个元素都执行了测试函数,没有符合的元素,则返回undefined。

const arr = [1, 2, 3, 4]
const findItem = arr.find((item, index) => {
    return item > 2
})
const findItem1 = arr.find((item, index) => {
    return item > 5
})
console.log(findItem) // 3
console.log(findItem1) // undefined

findIndex((currentValue, index, arr) => {}) 用法和find()一样,不同的是不是返回数组内元素,而是返回符合测试函数判断的元素索引值,如果没有符合条件的元素返回 -1。

const arr = [1, 2, 3, 4]
const findItemIndex = arr.findIndex((item, index) => {
    return item > 2
})
const findItemIndex1 = arr.findIndex((item, index) => {
    return item > 5
})
console.log(findItemIndex) // 2
console.log(findItemIndex1) // -1

11. forEach()、map()、filter()、some()、every() 迭代方法,不改变原数组。

arr.forEach((currentValue , index , arr) => {}, thisValue) 对数组进行遍历循环,这个方法没有返回值。

  • 对于空数组不执行
  • [currentValue] 必须项,当前元素
  • [index] 可选。当前元素的索引值
  • [arr] 可选。当前元素所属的数组对象
  • [thisValue] 可选。传递给函数的值一般用 "this" 值。如果这个参数为空, "undefined" 会传递给 "this" 值。
const arr = [1,4,7,10];
arr.forEach((currentValue, index, arr) => {
    console.log(index + "--" + currentValue + "--" + (arr === Arr)) 
})
// 输出:
// 0--1--true
// 1--4--true
// 2--7--true
// 3--10--true  

arr.map((currentValue , index , arr) => {}, thisValue) 指“映射”,方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。(不会改变数组长度,和原数组长度保持一致)

const arr = [1, 2, 3]
const arr1 = arr.map((currentValue) => {
    return currentValue + 1
})
console.log(arr) // [1, 2, 3]
console.log(arr1) // [2, 3, 4]

arr.filter((currentValue , index , arr) => {}, thisValue) “过滤”功能,方法
创建一个新数组,其包含通过所提供函数实现的测试的所有元素。(可以改变数组长度,不必和原数组长度保持一致)

const arr = [1, 2, 3]
const arr1 = arr.filter((currentValue) => {
    return currentValue > 1
})
const arr2 = arr.filter((currentValue) => {
    return currentValue > '1'
})
console.log(arr) // [1, 2, 3]
console.log(arr1) // [2, 3]
console.log(arr2) // [2, 3]
  • arr1和arr2结果一致,可以看出函数支持弱等于,不是必须全等于

arr.some((currentValue , index , arr) => {}, thisValue) 判断数组中是否存在满足条件的项,只要有一项满足条件,就会返回true,不再往下执行。

const arr = [1, 2, 3]
const str = arr.some((currentValue) => {
    console.log(currentValue)
    return currentValue > 1
})
// 1
// 2
console.log(str) // true

arr.every((currentValue , index , arr) => {}, thisValue) 判断数组中的每一项是否都满足条件,全部符合就会返回true,否则false。

const arr = [1, 2, 3]
const str = arr.every((currentValue) => {
    return currentValue > 1
})
console.log(str) // false

12. reduce()、reduceRight() 归并方法,不改变原数组

  • 这两个方法都会迭代数组中的所有项,然后生成一个最终返回值。他们都接收两个参数,第一个参数是每一项调用的函数,函数接受四个参数分别是初始值,当前值,索引值,和当前数组,函数需要返回一个值,这个值会在下一次迭代中作为初始值。第二个参数是迭代初始值,参数可选,如果缺省,初始值为数组第一项,从数组第一个项开始叠加,缺省参数要比正常传值少一次运算。
  • [total] 必须项,初始值, 或者计算结束后的返回值。
  • [cur] 必须项,当前元素。
  • [index] 可选。当前元素的索引值
  • [arr] 可选。当前元素所属的数组对象
  • [initialValue] 可选。传递给函数的初始值。

arr.reduce((total , cur , index , arr) => {}, initialValue) 从数组的第一项开始,逐个遍历到最后
arr.reduceRight((total , cur , index , arr) => {}, initialValue) 从数组的最后一项开始,向前遍历到第一项

const arr = [1,2,3,4,5]
const result1 = arr.reduce((total,cur,index,arr) => {   
    console.log("total:" + total + ",cur:" + cur + ",index:" + index)
    return total + cur
})
console.log("结果:" + result1)
// 输出
// total:1,cur:2,index:1
// total:3,cur:3,index:2
// total:6,cur:4,index:3
// total:10,cur:5,index:4
// 结果:15
const result2 = arr.reduce((total,cur,index,arr) => {   
    console.log("total:" + total + ",cur:" + cur + ",index:" + index)
    return total + cur
},10)
console.log("结果:" + result2)
// 输出
// total:10,cur:1,index:0
// total:11,cur:2,index:1
// total:13,cur:3,index:2
// total:16,cur:4,index:3
// total:20,cur:5,index:4
// 结果:25
  • 从上面代码我们可以看出,当我们不给函数传递迭代初始值时初始值 total 为数组第一项,函数从数组第二项开始迭代;若我们给函数传递迭代初始值,则函数从数组第一项开始迭代。

3数组去重数组去重主要的5种方法,_邱志刚的博客-CSDN博客_数组去重的5种方法

数组去重的方法

一、利用ES6 set去重(ES6中最常用)

var arr = [1,1,8,8,12,12,15,15,16,16];
function unique (arr) {
  return Array.from(new Set(arr))
}

console.log(unique(arr))
 //[1,8,12,15,16]

不考虑兼容性,这种去重的方法代码少,这种方法还无法去掉"{}"空对象,后面的高阶方法会添加去掉重复"{}"的方法。

二、利用for嵌套for,然后splice去重(ES5中最常用)

var arr = [1, 1, 8, 8, 12, 12, 15, 15, 16, 16];

function unlink(arr) {
    for (var i = 0; i < arr.length; i++) {    // 首次遍历数组
        for (var j = i + 1; j < arr.length; j++) {   // 再次遍历数组
            if (arr[i] == arr[j]) {          // 判断连个值是否相等
                arr.splice(j, 1);           // 相等删除后者
                j--;
            }
        }
    }
    return arr
}
console.log(unlink(arr));

双层循环,外层循环元素,内层循环时比较值,值相同时,则删去这个值

3、利用indexOf去重

var arr = [1, 1, 8, 8, 12, 12, 15, 15, 16, 16];
function unlink(arr) {
    if (!Array.isArray(arr)) {
        console.log('错误!')
        return
    }
    var array = [];
    for (var i = 0; i < arr.length; i++) {    // 首次遍历数组
        if (array.indexOf(arr[i]) === -1) {   // 判断索引有没有等于
            array.push(arr[i])
        }
    }
    return array
}
console.log(unlink(arr));

新建一个空的结果数组,for循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组

4利用includes

var arr = [1, 1, 8, 8, 12, 12, 15, 15, 16, 16];
function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array =[];
    for(var i = 0; i < arr.length; i++) {
            if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值
                    array.push(arr[i]);
              }
    }
    return array
}
console.log(unique(arr))


5利用filter

var arr = [1, 1, 8, 8, 12, 12, 15, 15, 16, 16];
function unlink(arr) {
    return arr.filter(function (item, index, arr) {
        //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
        return arr.indexOf(item, 0) === index;
    });
}
console.log(unlink(arr));


4、闭包有什么用什么是闭包?闭包的优缺点? | 菜鸟教程 (runoob.com)

1、变量作用域

要理解闭包,首先要理解JavaScript的特殊的变量作用域

变量的作用域无非就两种:全局变量和局部变量

JavaScript语言的特别之处就在于:函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量

注意点:在函数内部声明变量的时候,一定要使用var命令,如果不用的话,实际上声明的是一个全局变量

2、如何从外部读取函数内部的局部变量?

处于种种原因,我们有时候需要获取到函数内部的局部变量,但是,上面已经说过了,正常情况下,这是办不到的,只有通过变通的方法才能实现

那就是在函数内部,再定义一个函数

function f1(){
    var n=999;
    function f2(){
        alert(n); // 999
    }
}
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的

这就是JavaScript语言特有的链式作用域结构(chain scope)

子对象会一级一级地向上寻找所有父对象的变量,所以,父对象的所有变量,对子对象都是可见的,反之则不成立

既然f2可以读取到f1中的局部变量,那么只要把f2作为返回值,我们不就可以再f1外部读取它的内部变量了吗

3、闭包的概念

上面代码中的f2函数,就是闭包

各种专业文献的闭包定义都非常抽象,我的理解是,闭包就是能够读取其他函数内部变量的函数。由于在JavaScript中,只有函数内部的子函数才能读取局部变量,所以说,闭包可以简单理解成定义在一个函数内部的函数

所以本质上,闭包是将函数内部和函数外部连接起来的桥梁

4、闭包的用途

闭包可以用在很多地方,它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个是让这些变量的值始终保存在内存中,不会在f1调用后被自动清除。

为什么这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1.因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制回收

这段代码中另一个值得注意的地方,就是 "nAdd=function(){n+=1}" 这一行,首先在 nAdd 前面没有使用 var 关键字,因此 nAdd 是一个全局变量,而不是局部变量。其次,nAdd 的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以 nAdd 相当于是一个 setter,可以在函数外部对函数内部的局部变量进行操作。

5使用闭包的注意点

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

(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当做对象(object)使用,把闭包当做它的公用方法(public method),把内部变量当做它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

5事件代理在捕获阶段的实际应用

可以在父元素层面阻止事件向子元素传播,也可以替子元素执行某些操作

6去除字符串首尾空格

使用正则(^\s*)|(\s*$)即可

7性能优化

减少HTTP请求

使用内容发布网络(CDN)

添加本地缓存

压缩资源文件

将CSS样式表放在顶部,把javascript放在底部(浏览器的运行机制决定)

避免使用CSS表达式

减少DNS查询

使用外部javascript和CSS

避免重定向

图片lazyLoad

8讲讲JS的语言特性JavaScript的语言特性_lsj960922的博客-CSDN博客_javascript特性

一脚本语言

JavaScript是一种解释型的脚本语言,C、C++等语言先编译后执行,而JavaScript是在程序的运行过程中逐行进行解释。

二、基于对象

JavaScript是一种基于对象的脚本语言,它不仅可以创建对象,也能使用现有的对象

三、简单

JavaScript语言中采用的是弱类型的变量类型,对使用的数据类型未做出严格的要求,是基于Java基本语句和控制的脚本语言,其设计简单紧凑

四、动态性

JavaScript是一种采用事件驱动的脚本语言,它不需要经过web服务器就可以对用户的输入做出相应,在访问一个网页时,鼠标在网页中点击或上下移、窗口移动等操作JavaScript都可直接对这些事件给出相应响应

五、跨平台性

JavaScript脚本语言不依赖于操作系统,仅需要浏览器的支持。因此一个JavaScript脚本在编写后可以带到任意机器上使用,前提上机器上的浏览器支 持JavaScript脚本语言,目前JavaScript已被大多数的浏览器所支持。不同于服务器端脚本语言,例如PHP与ASP,JavaScript主要被作为客户端脚本语言在用户的浏览器上运行,不需要服务器的支持。所以在早期程序员比较青睐于JavaScript以减少对服务器的负担,而与此同时也带来另一个问题:安全性。而随着服务器的强壮,虽然程序员更喜欢运行于服务端的脚本以保证安全,但JavaScript仍然以其跨平台、容易上手等优势大行其道。同时,有些特殊功能(如AJAX)必须依赖Javascript在客户端进行支持。随着引擎如V8和框架如Node.js的发展,及其事件驱动及异步IO等特性,JavaScript逐渐被用来编写服务器端程序。

9JS实现跨域JavaScript实现跨域请求_Abraverman的博客-CSDN博客_javascript 跨域请求

一、什么是跨域请求

简单来说就是一个域通过某种方式请求到另一个域的数据,比如说百度请求京东的数据!这种方式适合有合作的两个公司,方法有以下

二、具体方法jsonp、cors(具体可以点开链接、、、、、)

10JS的命名方式js命名规范 - 简书 (jianshu.com)

11JS深度拷贝一个元素的具体实现js中实现深拷贝的4种方法_WikY-CSDN博客_js深拷贝

原生js中递归函数拷贝

将数据中所有的数据拷贝下来,对拷贝之后的数据进行修改不会影响到原数据,两个对象或两个数组不共享一块内存

  <script>
    let obj={
      abc:'123',
      def:[{a:1,b:2,c:3},{q:8,w:9}],
      qwe:{e:4,f:5}
    }
    //需求将obj这个对象拷贝出一个新对象修改新对象的值不会影响原对象的值
    //定义一个函数
    
    function copyobj(oldobj){
      //定义一个变量接收新对象
      let newObj=null
      //判断这个对象还是数组
      //1.如果是数组 
      if(oldobj instanceof Array){
        newObj=[]
        oldobj.forEach(item => {
          newObj = copyobj(item)
        }); 
        //2.如果是对象
      }else if(oldobj instanceof Object){
        newObj={}
        for(var i in oldobj){
         newObj[i]=copyobj(oldobj[i])
        }
      }else {
        newObj=oldobj
      }
    return newObj
    }
    let news=copyobj(obj)
    console.log(news);
  </script>

Object.create()

Object.create()实现的是深拷贝通过原型链的方式

new Object()实现的是浅拷贝修改原数据新拷贝的数据也会随之改变

<script>
    let a1={
      abc:'123',
      def:[{a:1,b:2,c:3},{q:8,w:9}],
      qwe:{e:4,f:5}
    }
//new Object() 实现的浅拷贝修改原数据新拷贝的数据也会随之改变
    let b1=new Object(a1)
    console.log(b1); //{abc:'123',def:[{a:1,b:2,c:3}{q:8,w:9}],qwe:{e:4,f:5}}
    b1.abc="qqqq"
    console.log(a1,b1); 
//a1对应的值{abc:'qqqq',def:[{a:1,b:2,c:3}{q:8,w:9}],qwe:{e:4,f:5}}
//b1对应的值{abc:'qqqq',def:[{a:1,b:2,c:3}{q:8,w:9}],qwe:{e:4,f:5}}

//Object.create() 实现的是深拷贝通过原型链的方式
    var a = { rep: 'apple' }
    var b = Object.create(a)
    console.log(b); //{ rep: 'apple' }
    b.rep='www'
    console.log(a,b);// { rep: 'apple' },{ rep: 'www' }
</script>

JQuery中$.extend

jQuery.extend() 函数用于将一个或多个对象的内容合并到目标对象。

$.extend( [deep ], target, object1 [, objectN ] )

deep 可选,Boolean类型 指示是否深度合并对象,默认为false。如果该值为true

target Object类型 目标对象

object1 可选。 Object类型 第一个被合并的对象。

var obj = {};
var object = { name: 'Bruce', career: "doctor" };
jQuery.extend(deep,obj, object); //obj = { name: 'Bruce', career: "doctor" }

函数库lodash,提供cloneDeep实现

1.下载相关库

npm i --save lodash

2.在相关文件中引入

import _ from "lodash"

3.调用 _.cloneDeep() 方法实现深拷贝

<script>
import _ from "lodash"
var objects = [{ 'a': 1 }, { 'b': 2 }];
var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]); //false
</script>


12重排和重绘重绘(redraw或repaint),重排(reflow) - cencenyue - 博客园 (cnblogs.com)

浏览器运行机制图

浏览器的运行机制:

1、构建DOM树(parse):渲染引擎解析HTML文档,首先将标签转换成DOM树中的DOM node(包括js生成的标签)生成内容树(Content tree/DOM tree)

2、构建渲染树(construct):解析对应的CSS样式文件信息(包括js生成的样式和外部css文件),而这些文件信息以及HTML中可见的指令(如<b></b>),构建渲染树(Rendering Tree/Frame Tree);

3、布局渲染树(reflow/layout)从根节点递归调用,计算每一个元素的大小、位置等,给出每个节点所应该在屏幕上出现的精确坐标;

4、绘制渲染树(paint/repaint)遍历渲染树,使用UI后端层来绘制每个节点。

重绘:当盒子的位置、大小以及其他属性例如颜色、字体大小等都确定下来之后,浏览器便把这些元素都按照各自的特性绘制一遍,将内容呈现在页面上。

重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。

触发重绘的条件:改变元素外观属性,如color,background-color等

注意:table以及其内部元素可能需要多次计算才能确定好其在渲染树中节点的属性值,比同等元素要多花两倍时间,这就是我们尽量避免使用table布局页面的原因之一

重排(重构/回流/reflow):当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建, 这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。

重排和重绘的关系:在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。所以重排必定引起重绘,但重绘不一定hi引发重排。

触发重排的条件:

任何页面布局和几何属性的改变都会触发重排,比如:

  1、页面渲染初始化;(无法避免)

  2、添加或删除可见的DOM元素;

  3、元素位置的改变,或者使用动画;

  4、元素尺寸的改变——大小,外边距,边框;

  5、浏览器窗口尺寸的变化(resize事件发生时);

  6、填充内容的改变,比如文本的改变或图片大小改变而引起的计算值宽度和高度的改变;

  7、读取某些元素属性:(offsetLeft/Top/Height/Width, clientTop/Left/Width/Height, scrollTop/Left/Width/Height, width/height, getComputedStyle(), currentStyle(IE) )

重绘重排的代价:耗时,导致浏览器卡慢。

优化:  

1、浏览器自己的优化:浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。

2、我们要注意的优化:我们要减少重绘和重排就是要减少对渲染树的操作,则我们可以合并多次的DOM和样式的修改。并减少对style样式的请求。

(1)直接改变元素的className

(2)display:none;先设置元素为display:none;然后进行页面布局等操作;设置完成后将元素设置为display:block;这样的话就只引发两次重绘和重排;

(3)不要经常访问浏览器的flush队列属性;如果一定要访问,可以利用缓存。将访问的值存储起来,接下来使用就不会再引发回流;

(4)使用cloneNode(true or false) 和 replaceChild 技术,引发一次回流和重绘;

(5)将需要多次重排的元素,position属性设为absolute或fixed,元素脱离了文档流,它的变化不会影响到其他元素;

(6)如果需要创建多个DOM节点,可以使用DocumentFragment创建完后一次性的加入document;

(7)尽量不要使用table布局

13JS的全排列JS实现全排列_weixin_33912445的博客-CSDN博客

14跨域的原理

跨域,是指浏览器不能执行其他网站的脚本,它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制,那么只要协议、域名、端口有任何一个不同,都被当做不同的域。跨域原理:即通过各种方式,避免浏览器的安全限制。

15不同数据类型的值的比较,是怎么转换的,有什么规则js之数据类型的比较_weixin_30920597的博客-CSDN博客

==

比较:返回一个布尔值,相等返回true,不相等返回false

允许不同数据类型之间的比较

如果是不同类型的数据比较,会默认进行数据类型之间的转换;

    console.log(1 == 1);// true
    console.log(1 == "1");// true
    console.log(1 == "1px");//false
    console.log(1 == true);//true
    console.log([]==false)// true
    console.log([]==0)// true
    console.log(100 == false);// false
    console.log(188 == "188");// true
    console.log(false == " ");// true

如果是对象数据类型的比较,比较的是空间地址

    console.log({}==[])// false
    console.log({}=={})// false
    console.log([]=="")// true
    console.log([]==0)// true

数据类型比较规律

  1. 对象 ==布尔: 对象先转字符串再转数字,布尔转数字;
  2. 对象==对象: 比较是空间地址;
  3. 对象==数字 对象先转字符串,再转数字
  4. 对象==布尔: 对象先转字符串,再转数字,布尔转数字
  5. 对象==字符串 : 对象转字符串,字符串比较
  6. 布尔==字符串 : 布尔转数字,字符串转数字
  7. 布尔==数字: 布尔转数字
  8. 字符串==数字: 字符串转数字

  1. { } 对象toString 转换成字符串结果是"[object Object]"
    !(取反) : 会先把后面的值进行去布尔,然后再取反,最后比较

        console.log(![] == [])// true
        console.log([]==[])// false
        console.log(![]==false);//true
        console.log({a:1}=="[object Object]")// true
        console.log("12px" == 12);

===
绝对比较; 只要数据类型不一样,那么返回false;
console.log(1 === 1);// true
console.log(1 === true);// false
console.log(1 === "1");// false

类型转换JS中的数据类型和转换 - Java填坑笔记 - 博客园 (cnblogs.com)

16null==undefined为什么undefined == null的正确解释_个人前端记录-CSDN博客

要比较相等性之前,不能将 null 和 undefined 转换成其他任何值,并且规定null 和 undefined 是相等的。

null 和 undefined都代表着无效的值。

17this的指向 哪几种this的指向有哪几种情况?_huyao的博客-CSDN博客_this指向有几种

作为函数调用,非严格模式下,this指向window,严格模式下,this指向undefined

作为某对象的方法使用,this通常指向调用的对象

使用apply、call、bind可以绑定this的指向

在构造函数中,this指向新创建的对象

箭头函数没有单的this值,this在箭头函数创建时确定,它与声明所在的上下文相同

18 暂停死区

在代码块内,使用let、const命令声明变量之前,该变量都是不可用的,这在语法上,称为暂时性死区

19AngularJS双向绑定原理AngularJs 双向绑定原理(数据绑定机制) - 简书 (jianshu.com)

AngularJs为scope模型上设置了一个监听队列,用来监听数据变化并更新view。每次绑定一个东西到view(html)上时AngularJs就会往$watch队列里插入一条$watch,用来检测它监视的model里是否有变化的东西。当浏览器接收到可以被angular context处理的事件时,$digest循环就会触发。$digest会遍历所有的$watch。从而更新DOM

$watch

这有点类似于我们的观察者模式,在当前作用域$scope下,我们创建一个监控器$watchers和一个监听器$watch,$watchers负责管理所有的$watch,当我们每次绑定到UI上的时候就自动创建一个$watch,并把它放到$watchers。

controller.js

app.controller('MainCtrl', function($scope) {
  $scope.Hello = "Hello";
  $scope.world = "World";
});

index.html

<div>{{Hello}}</div>

这里,即使我们在$scope上添加了两个变量,但是只有一个绑定在了UI上,因此这里只生成一个$watch

$digest

当浏览器接收到可以被angular context处理的事件时,$digest循环就会触发。$digest将会遍历我们的$watch,如果$watch没有变化,这个循环检测将停止,如果有至少一个更新过,这个循环就会再次触发,直到所有的$watch都没有变化。这样就能够保证每个model都已经不会再变化。这就是脏检查(dirty checking)机制

controller.js

app.controller('MainCtrl', function() {
  $scope.name = "Foo";

  $scope.changeFoo = function() {
      $scope.name = "Bar";
  }
});

index.js

<div>{{ name }}</div>
<button ng-click="changeFoo()">Change the name</button>

当我们按下按钮

浏览器接收到一个事件,进入angular context.

$digest循环开始执行,查询每个$watch是否变化

由于监视$scope.name的$watch报告了变化,它会强制再执行一次$digest循环。

新的$digest循环没有检测到变化

更新与$scope.name新值相应部分的DOM

$apply

$apply我们可以直接理解为刷新UI,如果当事件触发时,你调用$apply,它会进入angular context,如果没有调用就不会进入,之后的$digest检测机制就不会触发

app.directive('clickable', function() {
    return {
      restrict: "E",
      scope: {
        foo: '='
      },
      template: '<ul style="background-color: lightblue"><li>{{foo}}</li></ul>',
      link: function(scope, element, attrs) {
        element.bind('click', function() {
          scope.foo++;
          console.log(scope.foo);
        });
      }
    }
});

当我们调用clickable指令的时候,我们可以看到foo的值增加了,但是界面上显示的内容并没有改变。$digest脏检测机制没有触发,检测foo$watch就没有执行。

$apply()方法的两种形式

1) 无参

$scope.$apply();
element.bind('click', function() {
  scope.foo++;
  //if error
  scope.$apply();
});

当我们使用这种形式的时候,如果在scope.$apply之前程序发生异常,那scope.$apply没有执行,界面就不会更新

2) 有参

$scope.$apply(function(){
    ...
})
element.bind('click', function() {
  scope.$apply(function() {
      scope.foo++;
  });
})

如果用这种形式,即使后面的发生异常,数据还是会更新。

在 AngularJS 中使用 $watch

常用的使用方式:

$scope.name = 'Hello';
$scope.$watch('name', function(newValue, oldValue) {
    if (newValue === oldValue) { return; } 
    $scope.updated++;
});

传入到$watch()中的第二个参数是一个回调函数,该函数在name的值发生变化的时候会被调用。

如果要监听的是一个对象,那还需要第三个参数:

$scope.data.name = 'Hello';
$scope.$watch('data', function(newValue, oldValue) {
    if (newValue === oldValue) { return; } 
    $scope.updated++;
}, true);

表示比较的是对象的值而不是引用,如果不加第三个参数true,在 data.name 变化时,不会触发相应操作,因为引用的是同一引用。

总结

1) 只有在$scope变量绑定到页面上,才会创建 $watch

2) $apply决定事件是否可以进入angular context

3) $digest 循环检查model时最少两次,最多10次(多于10次抛出异常,防止无限检查)

4) AngularJs自带的指令已经实现了$apply,所以不需要我们额外的编写

5) 在自定义指令时,建议使用带function参数的$apply

牛客:

Angular将双向绑定转换为一堆watch表达式,然后递归这些表达式检查是否发生过变化,如果变了则执行相应的watcher函数(指view上的指令,如ng-bind,ng-show等或是{{}})。等到model中的值不再发生变化,也就不会再有watcher被触发,一个完整的digest循环就完成了。

Angular中在view上声明的事件指令,如:ng-click、ng-change等,会将浏览器的事件转发给$scope上相应的model的响应函数。等待相应函数改变model,紧接着触发脏检查机制刷新view。

watch表达式:可以是一个函数、可以是$scope上的一个属性名,也可以是一个字符串形式的表达式。$watch函数所监听的对象叫做watch表达式。watcher函数:指在view上的指令(ngBind,ngShow、ngHide等)以及{{}}表达式,他们所注册的函数。每一个watcher对象都包括:监听函数,上次变化的值,获取监听表达式的方法以及监听表达式,最后还包括是否需要使用深度对比(angular.equals())

20requestAnimationFrame怎么使用requestAnimationFrame - 简书 (jianshu.com)

在requestAnimationFrame之前,主要借助setTimeout/setInterval来编写JS动画,而动画的关键在于动画帧之间的时间间隔设置,这个时间间隔的设置有讲究,一方面要足够小,这样动画帧之间才有连贯性,动画效果才显得平滑流畅;另一方面要足够大,确保浏览器有足够的时间及时完成渲染。

大部分显示器的刷新频率是60Hz,即每秒钟重绘60次,大多数浏览器都会对重绘操作的频率加以限制,使其不超过显示器的刷新频率。一般最平滑流畅的动画的时间间隔是 1000ms/60,约为 16.7 ms.

时间间隔对于动画非常重要,但是 setTimeout/ setInterval 的显著缺陷就是设定的时间并不精确,它们只是在设定的时间后将相应任务添加到任务队列中,而任务队列中如果还有前面的任务尚未执行完毕,那么后添加的任务就必须等待,这个等待的时间造成了原本设定的动画时间间隔不准。

requestAnimationFrame 应运而生,它采用的是系统时间间隔(约16.7ms),保持最佳绘制效果与效率,使各种网页动画有一个统一的刷新机制,从而节省系统资源,提高系统性能。
MDN关于requestAnimationFrame:告诉浏览器,希望执行一个动画,并要求浏览器在下次重绘之前使用指定的回调函数更新动画。(如果想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 window.requestAnimationFrame(回调函数),这样动画才有很多帧,否则只有两帧(初始状态算一帧,回调函数执行一次的结果算一帧)且在16.7ms内完成就失去了动画效果 )。
回调函数会被自动传入 'DOMHighResTimeStamp'参数(时间戳,十进制数,单位是毫秒,最小精度为1ms),该参数指示当前被 requestAnimationFrame() 排序的回调函数被触发的时间。在同一个帧中的多个回调函数,每个都会被传入一个相同的时间戳。

21有一个游戏叫做Flappy Bird,就是一只小鸟在飞,前面是无尽的沙漠,上下不断有钢管生成,你要躲避钢管。然后小明在玩这个游戏时候老是卡顿甚至崩溃,说出原因(3-5个)以及解决办法(3-5个)

原因可能是:

1.内存溢出问题。

2.资源过大问题。

3.资源加载问题。

4.canvas绘制频率问题

解决办法:

1.针对内存溢出问题,我们应该在钢管离开可视区域后,销毁钢管,让垃圾收集器回收钢管,因为不断生成的钢管不及时清理容易导致内存溢出游戏崩溃。

2.针对资源过大问题,我们应该选择图片文件大小更小的图片格式,比如使用webp、png格式的图片,因为绘制图片需要较大计算量。

3.针对资源加载问题,我们应该在可视区域之前就预加载好资源,如果在可视区域生成钢管的话,用户的体验就认为钢管是卡顿后才生成的,不流畅。

4.针对canvas绘制频率问题,我们应该需要知道大部分显示器刷新频率为60次/s,因此游戏的每一帧绘制间隔时间需要小于1000/60=16.7ms,才能让用户觉得不卡顿。

(注意因为这是单机游戏,所以回答与网络无关)

22es6新特性用过哪些ES6 新特性 - 前端杂货 - 博客园 (cnblogs.com)

1、let、const,block作用域

let允许创建块级作用域(最靠近的一个花括号内有效),不具备变量提升,不允许重复声明

// 块级作用域
{
  let a = 1
  console.log(a)  // 1
}
console.log(a) // Uncaught ReferenceError: a is not defined

// 变量不提升
{
  console.log(a) // Uncaught ReferenceError: Cannot access 'a' before initialization
  let a = 1
}

// 不能重复声明
{
  let a = 1
  let a = 2 // Uncaught SyntaxError: Identifier 'a' has already been declared
}

const允许创建块级作用域(最靠近的一个花括号内有效)、变量声明不提升,const在声明时必须被赋值,声明时大写变量

// 块级作用域
{
  const A = 1
  console.log(A)
}
console.log(A) // Uncaught ReferenceError: A is not defined

// 变量不提升
{
  console.log(A) // Uncaught ReferenceError: Cannot access 'A' before initialization
  const A = 1
}

// 声明时必须赋值
{
  const A // Uncaught SyntaxError: Missing initializer in const declaration
}

const 声明的变量都会被认为是常量,意思就是它的值被设置完成后就不能再修改了:

2箭头函数

箭头函数最直观的三个特点:

不需要function关键字来创建函数,省略return关键字,继承当前上下文的this关键字

ES6中,箭头函数就是函数的一种简写形式,使用括号包裹参数,跟随一个=>,紧跟着是函数体

// ES5写法
var getPrice = function() {
  return 4.55;
};
 
// ES6箭头函数写法,不需要return语句
var getPrice = () => 4.55

箭头函数不具备this变量指向改变:

// ES5
function Person() {
  this.age = 0;
 
  setInterval(function growUp() {
    // 在非严格模式下,growUp() 函数的 this 指向 window 对象
    this.age++;
  }, 1000);
}
var person = new Person();

// ES6
function Person(){
  this.age = 0;
 
  setInterval(() => {
    // |this| 指向 person 对象
    this.age++;
  }, 1000);
}
 
var person = new Person();

3函数参数默认值

ES6中允许你对函数参数设置默认值:

// ES5
var getFinalPrice = function (price, tax) {
  tax = tax || 0.7
  return price + price * tax;
}

// ES6
var getFinalPrice = function (price, tax = 0.7) {
  return price + price * tax;
}

4、Spread/Rest操作符

Spread/Rest操作符指的是运算符...:

// Spread,可以简单理解为分散
function foo(x,y,z) {
  console.log(x,y,z);
}
 
let arr = [1,2,3];
foo(...arr); // 1 2 3

// Rest,可以简单理解为合并
function foo(...args) {
  console.log(args);
}
foo( 1, 2, 3, 4, 5); // [1, 2, 3, 4, 5]

5、对象词法扩展

ES6允许声明在对象字面量时使用简写语法,来初始化属性变量和函数的定义方法,并且允许在对象属性中进行计算操作。

function getCar(make, value) {
  return {
    // 简写变量
    make,  // 等同于 make: make
    value, // 等同于 value: value

    // 属性可以使用表达式计算值
    ['make' + make]: true,

    // 忽略 `function` 关键词简写对象函数
    depreciate() {
      this.value -= 2500;
    },
    // ES5写法
    depreciateBak: function () {
      console.log(this['make' + this.make])
    }
  }
};

6、二进制和八进制字面量

ES6支持二进制和八进制的字面量,通常在数字前面添加0o或者0O即可将其转换为八进制值:

let oValue = 0o10;
console.log(oValue); // 8
 
let bValue = 0b10; // 二进制使用 `0b` 或者 `0B`
console.log(bValue);//2

7、对象和数组的解构

解构可以避免在对象赋值时产生中间变量:

// 对象
const student = {
    name: 'Sam',
    age: 22,
    sex: '男'
}
// 数组
// const student = ['Sam', 22, '男'];

// ES5;
const name = student.name;
const age = student.age;
const sex = student.sex;
console.log(name + ' --- ' + age + ' --- ' + sex);

// ES6
const { name, age, sex } = student;
console.log(name + ' --- ' + age + ' --- ' + sex);

8对象超类

ES6允许在对象中使用super方法:

var parent = {
  foo() {
    console.log("Hello from the Parent");
  }
}
 
var child = {
  foo() {
    super.foo();
    console.log("Hello from the Child");
  }
}
 
Object.setPrototypeOf(child, parent);
child.foo(); // Hello from the Parent
             // Hello from the Child

9、模块语法和分隔符

一种十分简洁的方法组装一堆字符串和变量

let user = 'Barret';
// ``作为分隔符,${ ... }用来渲染一个变量
console.log(`Hi ${user}!`);// Hi Barret!

10、for...of VS for...in

for...of用于遍历一个迭代器,如数组:

let letters = ['a', 'b', 'c'];
letters.size = 3;
for (let letter of letters) {
  console.log(letter);
}
// 结果: a, b, c

for...in用来遍历对象中的属性(只能访问可枚举的)

let letters = ['a', 'b', 'c'];
let letters = ['a', 'b', 'c'];
letters.size = 3;
for (let letter of letters) {
  console.log(letter);
}
// 结果: a, b, c

11 类

ES6中有class语法,值的注意的是,这里的class不是新的对象继承模型,它只是原型链的语法糖表现形式。

函数中使用static关键字定义构造函数的方法和属性:

class Student {
  constructor() {
    console.log("I'm a student.");
  }
 
  study() {
    console.log('study!');
  }
 
  static read() {
    console.log("Reading Now.");
  }
}
 
console.log(typeof Student); // function
let stu = new Student(); // "I'm a student."
stu.study(); // "study!"
stu.read(); // "Reading Now."

类中的继承和超集:

class Phone {
  constructor() {
    console.log("I'm a phone.");
  }
}
 
class MI extends Phone {
  constructor() {
    super();
    console.log("I'm a phone designed by xiaomi");
  }
}
 
let mi8 = new MI();

extends 允许一个子类继承父类,需要注意的是,子类的constructor 函数中需要执行 super() 函数。
当然,你也可以在子类方法中调用父类的方法,如super.parentMethodName()。
在 这里 阅读更多关于类的介绍。

有几点值得注意的是:

  • 类的声明不会提升(hoisting),如果你要使用某个 Class,那你必须在使用之前定义它,否则会抛出一个 ReferenceError 的错误
  • 在类中定义函数不需要使用 function 关键词

23什么是按需加载为什么需要按需加载? - 知乎 (zhihu.com)

按需加载时前端性能优化的一大措施,顾名思义,按需加载就是根据需要去加载资源,在js中,我们一般通过一些用户行为或者定时任务去触发一些加载动作,比如但不限于以下几个情况:鼠标点击、输入文字、拉动滚动条、鼠标移动、窗口大小更改等。加载的文件,可以是js、图片、CSS、HTML等。这就是按需加载

为什么需要按需加载

我们都知道,浏览器在同一时间内的可以发出的请求数有限制,所以这也是我们采用第三方打包工具将多个文件打包为一个文件的意义。但是多个文件打包为一个文件时包又比较大,一次性下载下来的速度就比较慢,仍然会有刚进入单页面系统产生首页白屏时间较长的情况。这种用户体验也不好。按需加载可用较好的去解决这些问题。

24说一下什么是virtual dom一次关于 Vue 的自我模拟面试 - 知乎 (zhihu.com)

Virtual DOM是DOM节点在JavaScript中的一种抽象数据结构,之所以需要虚拟DOM,是因为浏览器中操作DOM的代价比较昂贵,频繁操作DOM会产生性能问题。虚拟DOM的作用是在每一次响应式数据发送变化引起页面渲染时,Vue对比更新前后的虚拟DOM,匹配找出尽可能少的需要更新的真实DOM,从而达到提升性能的目的。

25webpack用来干什么的什么是WebPack,为什么要使用它? - 逗伴不是瓣 - 博客园 (cnblogs.com)

WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其打包为合适的格式以供浏览器使用。

为什要使用WebPack

今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包。为了简化开发的复杂度,前端社区涌现出了很多好的实践方法

a:模块化,让我们可以把复杂的程序细化为小的文件;

b:类似于TypeScript这种在JavaScript基础上拓展的开发语言:使我们能够实现目前版本的JavaScript不能直接使用的特性,并且之后还能能装换为JavaScript文件使浏览器可以识别;

c:scss,less等CSS预处理器

26ant-design优点和缺点

优点:组件非常全面,样式效果也都比较不错。

缺点:框架自定义程度低,默认UI风格修改困难。

27JS中继承实现的几种方式JS实现继承的几种方式 - 幻天芒 - 博客园 (cnblogs.com)

既然要实现继承,那么首先我们得有一个父类,代码如下:

// 定义一个动物类
function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

1、原型链继承

核心:将父类的实例作为子类的原型

function Cat(){ 
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true

特点:

1非常纯粹的继承关系,实例是子类的实例,也是父类的实例

2父类新增原型方法/原型属性,子类都能访问到

3,简单,易于实现

缺点:

  1. 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
  2. 无法实现多继承
  3. 来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)(详细请看附录代码: 示例1)
  4. 创建子类实例时,无法向父类构造函数传参

2、构造继承

核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值