原型链
- 每个复杂类型都拥有的属性不需要为每个对象都添加,而提取公用部分在内存中只保存一份,如: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__
中查找:- 存在,执行;
- 不存在,报错。
关于函数:
- 函数也是对象,使用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-image
和background-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)
}
网友评论