美文网首页
JavaScript 基础总结(2)

JavaScript 基础总结(2)

作者: 千反田爱瑠爱好者 | 来源:发表于2018-08-24 16:40 被阅读13次

    原型链

    • 每个复杂类型都拥有的属性不需要为每个对象都添加,而提取公用部分在内存中只保存一份,如:toString,valueOf
    • __proto__指向所有对象共有的属性
    • 每种类型的共有属性是不同的,比如toFixed只在所有Number共有。
    • String、Number、Boolean...等类型的共有属性集的超集为Object的共有属性集(所有对象的共有属性);
    • 共有属性即原型,proto共有属性的关系构成原型链:
    var s = new String()                        
    // 公式:var 对象 = new 函数()
    s.__proto__ === String.prototype             
    String.prototype.__proto__ === Object.prototype
    s.__proto__.__proto__ === Object.prototype
    
    // 对象的构造函数
    Function.__proto__ === Function.prototype
    Array.__proto__ === Function.prototype
    Object.__proto__ === Function.prototype
    
    // 从对象s的属性到String类型的共有属性(__proto__),再到Object的共有属性(__proto__.__proto__)构成一条原型链
    // 其中prototype是浏览器window对象所指的复杂类型(Number、String...)事先准备好的,自定义对象的__proto__用于引用对应类型的prototype
    // 所以String.prototype是String的共有属性,s.__proto__是String共有属性的引用
    

    对于一个变量是否存在某属性,会顺着原型链依次查找,当执行某方法或获取某属性(如o.toString)时,会先判断是否对象:

    • 不是对象,则做临时转换包装成对象执行方法;
    • 是对象,则检查该对象中是否定义了该方法:
      • 存在,直接执行;
      • 不存在,在__proto__中查找:
        • 存在,执行;
        • 不存在,报错。
    __proto__

    关于函数:

    • 函数也是对象,使用new创建函数即调用构造函数,创建了一个函数实例,有f.__proto__ === Function.prototype
    • 构造函数的原型对象prototype都时由Object构造出来,所以有Function.prototype.__proto__ === Object.prototype
    Object.__proto__ === Function.prototype     // Function是Object的构造函数
    Object.prototype.__proto__ === null         // Object.prototype已经是末端,不存在其引用的共有属性
    

    数组

    Array基本用法:构造数组对象

    • let a = Array(3)let a = new Array(3):创建长度为3,每个元素都是undefined的数组,a.length === 3;
    • let a = Array(3, 4, 5)let a = new Array(3, 4, 5)let a = [3, 4, 5]:创建长度为3,元素为3、4、5的数组;

    数组与对象

    当声明一个数组let a = [1, 2, 3]或一个对象let b = {0: 1, 1: 2, 3: 3, length: 3}其含义是:

    a[0] === b[0] === 1
    a[1] === b[1] === 2
    1[2] === b[2] === 3
    a.length === b.length === 3
    a.__proto__ === Array.prototype    // 数组的共有属性
    b.__proto__ === Object.prototype   // 对象的共有属性
    

    但前者称为数组、后者称为对象的根本原因是共有属性(原型链)不一样,当向数组加入数值以外的key:

    a.xxx = 1
    a.yyy = 2
    

    使用两种方式遍历会出现不同结果():

    for (let i = 0; i < a.length; i++) {
        console.log(a[i])
    }    // 输出1, 2, 3,不关心是否数组,只要存在数值索引即可这样遍历(以对象方式定义也是一样)
    for (let key in a) {
        console.log(key)
    }    // 输出1, 2, 3, xxx, yyy
    

    伪数组

    当对象的原型链中没有Array.prototype则称为伪数组:

    let a = {0: 1, 1: 2, 2: 3, length: 4}
    

    常用的伪数组是arguments,表示向函数传入的参数(不能执行push等方法):

    function f() {
        console.dir(arguments)
    }
    f(1, 2, 3)
    

    常用API

    a.forEach(function(x, y){    // 对数组中的每个元素执行传入的匿名函数
        console.log('value', x)    // 注意顺序,两个参数必然为value-key-array
        console.log('key', y)
    })        
    
    function forEach(array, func) {    // 等价于向函数传入两个参数(数组和函数)
        for (let i = 0; i < array.length; i++)
            func(array[i], i)
    }
    
    a.sort(function(x, y){return x - y})      // 按特定规则排序(从小到大,满足第一个参数比第二个参数大,返回Ture)
    a.join(', ')    // 数组转字符串(默认以逗号)
    a.concat(a)        // 拼接
    a.map(x => x % 2)             // 对数组执行特定函数,返回新的数组
    a.filter(x => x % 2 == 1)    // 过滤符合条件的元素
    a.reduce((x, y) => x + y)    // 归约数组元素
    

    函数

    声明方式

    // 具名函数
    function f(a, b) {return a + b}    // f.name === 'f'
    
    // 匿名函数(不能单独使用) 
    var f = function(a, b) {return a + b}    // f.name === 'f'
    
    // 具名函数赋值
    var f = function y(a, b) {return a + b}    // y不存在,f.name === 'y'
    
    // window.Function对象
    var f = new Function('x', 'y', 'return x + y')    // 最后一个参数为返回语句(可以动态定义),f.name === 'anonymous'
    
    // 箭头函数
    var f = (x, y) => x + y    // 只有一句话,而且不能带对象,f.name === 'f'
    

    本质

    • 函数是可反复调用的代码块,是可执行代码的对象(call方法);
    • f(a, b)f.call(undefined, a, b)的语法糖,f.call(undefined, a, b)才是真正的函数调用(函数调用的本质)。

    this与arguments

    每个函数都有自己的this和arguments参数,都要再函数调用(call)时才确定:

    • this:函数调用f.call(undefined, a, b)的第一个参数即为this(一般模式下浏览器中f.call(undefined),this为window;使用严格模式'use strict'则为undefined);
    • arguments:函数调用f.call(undefined, a, b)的第2到最后一个元素组成的伪数组。

    call stack

    每次发生函数调用的地方把当前执行函数入栈、执行内部逻辑,完成后再弹出,继续往下执行。

    function a() {
        console.log('a')
        b.call()
        return 'a'
    }
    
    function b() {
        console.log('b')
        c.call()
        return 'b'
    }
    
    function c() {
        console.log('c')
        return 'c'
    }
    
    a.call()
    

    递归

    function sum(n) {
        if (n == 1) {
            return 1
        } 
        else {
            return n + sum.call(undefined, n - 1)
        }
    }
    sum.call(undefined, 5)
    

    作用域

    作用域以树的形式表示,就近原则:

    var a = 1                // 全局作用域
    function f1() {          // 全局作用域
        f2.call()            
        console.log(a)       // undefined
        var a = 2            // 变量提升
        function f2() {      // f1作用域
            var a = 3        // f2作用域
            console.log(a)
        }
        f4.call()
    }
    function f4() {
        console.log(a)      // 1
    }
    // 在此处修改a的值,则会影响f4的输出,因为f4输出的是第一行声明的a,这个a在此处被修改,然后才执行f1内部的f4
    f1.call()
    console.log(a)
    

    a = 1更可能是对已声明变量赋值,从当前作用域开始向父作用域查找(先检查当前作用域中前面代码是否有声明、是否存在变量提升),直到当全局作用域都没有声明,才会认为是声明且赋值。

    函数非当场执行,相关变量就有被修改的可能,经典易错题:

    // 假设存在6个<li>标签
    
    var liTags = document.querySelectorAll('li')
    for (var i = 0; i < liTags.length; i++) {
        liTags[i].onclick = function() {
            console.log(i)
        }
    }
    
    // 当点击li标签时,输出的应该是6
    // console.log(i)所输出的i是for循环的i,这个i的值在for循环结束(也就是为li标签加上onclick事件)时被修改为6,所以后续每再次访问结果都为6
    

    闭包

    如果一个函数使用其作用域范围以外的变量,则这个函数、这个变量称为闭包:

    var a = 1
    function f4() {
        console.log(a)
    }
    
    • 可以从外部读取函数内部的变量:
    function f1() {
        var n = 9;
        function f2() {
            console.log(n);
        }
        return f2;
    }
    var f2 = f1();  // 函数f1的返回值是函数f2
    f2();           // f2可以读取f1的内部变量,所以调用f2时就可以获取f1的内部变量n
    
    • 让变量始终保持在内存中:
    function f1(n) {
        return function () {
            return n++;
        };
    }
    var a = f1(1);
    a()    // 1
    a()    // 2
    a()    // 3,内部变量记录每次调用结果会被记录
    
    • 可以封装对象的私有属性和私有方法
    function f1(n) {
        return function () {
            return n++;
        };
    }
    var a1 = f1(1)
    var a2 = f2(2)
    a1()
    a1()
    a2()
    a2()    // 每次调用a1和a2返回的结果都不同,因为a1和a2内部变量是相互独立的,会返回各自的内部变量
    

    继承

    JS函数可以产生对象,因此也可以作为类:

    function Human(name) {
        this.name = name
    }
    let person = new Human("ywh")
    

    继承即让子类具有父类的属性和方法:

    let a = new Array()
    a.push()        // 实例属性,源于Array.prototype,a的原型中
    a.valueOf()     // 继承属性,源于Array.prototype.__proto__,a的原型的原型中
    
    • 构造函数都有prototype属性,用于存放共有属性对象(如Object的toString,valueOf)的地址;
    • 类(也是函数)和对象的__proto__指向其原型(创造它的类的)prototype;
    • 当使用let obj = new Func()创建一个对象时,实际上依次执行了:
      • 产生一个空对象
      • this = 空对象
      • this.__proto__ = Func.prototype
      • Func.call(this, ...)
      • return this

    ES5实现继承(修改原型链性能损耗比较大)

    // 父类函数
    function Human(name) {      
        this.name = name
    }
    Human.prototype.run = function () {
        console.log(this.name + "跑")
        return undefined
    }
    
    // 子类函数
    function Man(name) {            
        Human.call(this, name)      // 把this传入Human父类函数,因此在父类函数内部的this就是此处的this
        this.gender = '男'
    }
    
    // Man.prototype.__proto__ = Human.__proto__  // Man的原型链原是直接指向Object,现插入一层Human
    
    /**
        let object = new Man("x")
        object.__proto__ === Man.prototype
        object.__proto__.prototype === Human.prototype
        object.__proto__.__proto__.__proto__ === Object.prototype
        object.__proto__.__proto__.__proto__.__proto__ === null
    */
    
    // 由于IE不支持直接操作__proto__,其中插入原型链也可以利用new实现:
    var f = function () {}
    f.prototype = Human.prototype
    Man.prototype = new f()        
    
    // 为子类添加方法
    Man.prototype.fight = function () {
        console.log('攻击')
    }
    

    ES6实现

    class Human{
        constructor(name) {
            this.name = name
        }
        run(){
            console.log(this.name + "跑")
            return undefined
        }
    }
    class Man extends Human {       // 表示在Man的原型链中插入Human
        constructor(name){
            super(name)
            this.gender = '男'
        }
        fight(){
            console.log('攻击')
        }
    }
    

    MIXIN

    将一个对象的属性复制给另一个对象

    let mixin = function(dest, src) {
        for(let key in src) {
            dest[key] = src[key]
        }
    }
    

    也可以使用Object.assign实现:

    Object.assign(dest, src)
    

    柯里化

    把函数参数固定下来转化成偏函数:

    let f = function(x, y) {
        return x + y
    }
    let g = function(y) {
        return f(1, y)
    }
    f(1, 2)
    g(2)
    

    也可以多次传参:

    var cache = []
    var add = function(n) {
        if (n === undefined) {
            return cache.reduce((p, n) => p + n, 0)
        }
        else {
            cache.push(n)
            return add
        }
    }
    
    add(1)(2)(3)...()
    

    高阶函数

    至少满足一个条件即为高阶函数

    • 接受一个或多个函数作为输入
    • 输出一个函数
    function add(x, y) {
        return x + y
    }
    
    f = Function.prototype.bind.call(add, undefined, 1)     // 把其中一个参数固定为1,也实现为柯里化
    f(2)
    

    Web性能优化

    浏览器请求网页的过程及优化:

    • 读取缓存:Cache-Control
    • DNS查询:把资源放在同一个域名可以减少DNS查询次数(需要与第四点权衡)
    • 建立TCP连接:使用keepalive复用TCP连接、使用HTTP/2.0实现多路复用
    • 发送HTTP请求:减少cookies体积、增加资源域名数量使浏览器同时发送更多HTTP请求
    • 接收HTTP响应:通过ETag避免未无需更新的资源的接收、Content-Encoding: gzip压缩传输(CPU解压)
    • 接收完成,解析HTML
      • 根据DOCTYPE(不写/写错DOCTYPE会导致浏览器判断损耗性能),逐行解析HTML代码(尽量减少标签)并渲染(Chrome不会直接开始渲染,而是等待CSS下载完成)
      • 在渲染HTML标签过程中会并行下载CSS/JS、串行解析(下载JS过程会阻塞HTML渲染、Chrome中下载CSS过程会阻塞HTML渲染)

    其他:

    • 使用CDN(内容分发网络)优化JS/CSS资源下载速率;
    • CSS放在<head>(提早下载)、JS放在<body>底部(避免阻塞其他标签渲染);
    • CSS可以合并减少下载文件数量;
    • 使用雪碧图合并多个背景图片(background-imagebackground-position控制显示不同部分);
    • 对于长页面结合使用懒加载(滚动动态加载)、预加载;
    • 避免空src的图片(依然会发起请求,可以指定src="about:blank");
    • 使用事件委托减少监听器:
    let liList = document.querySelectorAll('li')
    liList[0].onclick = () => console.lot(1)
    liList[1].onclick = () => console.lot(1)
    liList[2].onclick = () => console.lot(1)
    
    // 可以直接监听其父元素
    ul.onclick = (e) => {
        if (e.target.tagName === "LI")
            console.log(1)
    }
    

    相关文章

      网友评论

          本文标题:JavaScript 基础总结(2)

          本文链接:https://www.haomeiwen.com/subject/aebbiftx.html