JavaScript 基础总结(2)

原型链

  • 每个复杂类型都拥有的属性不需要为每个对象都添加,而提取公用部分在内存中只保存一份,如: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__中查找:
      • 存在,执行;
      • 不存在,报错。
11445944-32c1a949fde6d872.png
__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)
}

猜你喜欢

转载自blog.csdn.net/weixin_34072458/article/details/87239511