JS 核心基础原理

1,数据类型,隐式转换、循环

  • 数字与NAN运算 +-*/% 都是NAN
  • 数字与字符串数字比较,把字符串转换为数字
  • 字符串比较,从左向右,依次比较 ASCII
    • 例如:'89' > '9',比较它们的ASCII码值
  • NAN 与包括自己的任何东西都不相等 ==
  • 条件互斥的情况,使用 else-if,最后的 else 兜底异常意外
  • if 处理范围判断, switch 处理固定值
  • 假值:undefined、null、NAN、""、0、false
  • &&,且运算符,遇真往后走,遇见假值或走到最后,就返回当前值; 找假返回;
    • let a = 1 && 2,a为2
    • let b = 1 && null && 2,b为null
  • ||,或返回值,遇假往后走,遇见真值或走到最后,就返回当前值; 找真返回;
    • let a = null || 0,a=0
    • let b = null || 1 || 0,b=1
  • , 逗号运算符
    • 返回最后的那个值
    • console.log(1,2,3),打印3
  • for 循环
for(let i= 1;i<10;i++){
	console..log(i)
}
// 本质
// 先声明 i
let i=1if(i<10){
	console.log(i)
}
i++
if(i<10){
	console.log(i)
}
// 循环 判断条件 执行 直到不满足条件
i++
if(i<10){
	console.log(i)
}
  • for 和 while 循环
// for 循环换个写法,这样写也是成立并且可执行的
let i = 1;
for(;i<10;){
	console.log(i);
	i++;
}
// while 循环
let i = 1
while(i<10){
	console.log(i)
	i++
}
这样看来,其实for循环和while循环是一样的
  • 终止for循环
    • break ,终止循环
    • continue ,跳过本次循环
// 在打印十次后,终止循环
let i = 1 
for(;i;){
	console.log(i)
	i++
	if(i==11){
		// break
		// 或 修改条件为假值 0、undefined、null、""、false
		i = 0
	}
}
// 题目:() 括号内只能有一句,不准比较,{}内不能有i++ 、 i--
// 条件每次减一,为0时自动停止,判断条件为假时自动停止
let i = 10
for(;i--;){
	console.log(i)
}
  • 求 整除 和 个位数
    • 能否整除,a%b == 0
    • 个位数,a%10 = a的个位数
  • typeof 以前用法 typeof value,其实还可以 typeof(value),值写在括号内,规范,方便维护
    • typeof 得出的 'object',并不代表是一个对象,可以理解为代表是 引用类型 数据
    • typeof(1-‘1’) 、typeof(‘1’-‘1’),都是'number'
    • typeof(未定义的值),'undefined'
    • typeof(typeof(未定义的值)),'string',因为typeof的返回值是字符串
  • null 和 undefined
    • null 代表空值,typeof null = 'object',是历史遗留问题,为了初始化对象,可以理解为一个bug
    • undefined ,未定义或为赋值
    • undefined隐式转换数字是NAN,null是0
    • undefined 和 null 没有 toString() 方法
    • null == undefined ,true
  • 隐式转换
    • Number(null、false) = 0,isNAN(null、false) = false
    • Number(undefined) = NAN,isNAN(undefined、汉字字母) = true
    • parseInt(null、false、undefined) = NAN
  • 字符串有长度,数字没长度 length
  • *、/、%、-、递增、递减、比较,都存在隐式转换,字符串 ==> 数字
    • (+ '123') = 123
    • (- '123') = -123
    • 单一个加减号,也存在隐式转换
  • js中,默认不允许字符串多行(换行),模板字符串支持
let str = '123
		   abc'	// 错误,不支持普通字符串多行
let str2 = `123
			abc` // 模板字符串可以

2,函数

函数内部 let a = b =1,等同于b=1; let a = b,b是全局变量
如果不返回值,默认返回undefined

// 匿名函数表达式 声明函数 又称为匿名函数 函数字面量
let fn = function fn2(){
	// 内部可以使用fn2调用函数
	fn2()
}
此时,该函数会有一个name属性叫 fn2
但在函数外部要用 fn 才能调用函数
  • 函数有一个arguments内置对象,在函数内部可以拿到所有实参
    • 不管你的形参数量 够不够接受实参,甚至没有形参,arguments 都能获取所有实参
  • 函数有length属性,是它的形参个数
如果传递了实参,就可以在函数内部修改这个实参的值,
修改参数的值,arguments的值也会变
但是如果没有传这个实参的值,就不可以修改它的值
function fn(a,b){
	a = 5;
	b = 10
	console.log(a,b) // 5,10 , 这里的b并不再是形参b,而是一个全局变量
	console.log(arguments[0],arguments[1]) // 5,undefined ,更改参数值后,arguments中的参数值也会改变
	// 上面的a 并不再是真正意义上的arguments[0],但是它们存在一个映射关系
}
fn(1)
  • 初始化参数
function fn(a = 1 , b){
	console.log(a,b)
}
// 两个形参,a有默认值,但是我只想传一个参数给b,但是我如果直接传一个实参,就会被赋值给a
// 这个时候,可以穿一个undefined给形参a
fn(undefined,10)
当有形参默认值时,传入的实参和默认值之间,谁不是undefined就取谁的值,都不是就取实参
// 形参默认值的手动实现
function fn2(a,b){
	if(typeof(a)=='undefined'){
		// 传入的是undefined,就给他默认值
		a = 1
	}else{
		// 有真值,就使用真值
		a = arguments[0]
	}
	console.log(a)
	// 或
	// let a = arguments[0] || 1
}
fn2(undefined,10)
  • 全局变量

    • 全局作用域下,无论是先声明再赋值、还是直接赋值,变量都是全局变量,挂载在window上
    • 函数作用域内,变量不声明,直接赋值为全局作用域
  • 函数上下文预编译 AO

    • 1,寻找形参和变量声明
    • 2,实参赋值给形参
    • 3,寻找函数声明
    • 4,按照代码顺序执行
function fn(a){
	console.log(a)
	var a = 5
	console.log(a)
	function a(){}
	console.log(a)
	var b = function(){}
	console.log(b)
}
fn(100)

函数上下文预编译 变量提升 
AO:函数上下文对象
1,寻找形参和变量声明
	a = undefined
	b = undefined
2,实参赋值给形参
	a = 100
	b = undefiend
3,找函数声明
	a = function
	b = function
4,做完以上步骤,再按代码顺序执行
注意:提取AO时,不受条件语句if等影响,也不受 return 影响,因为预编译时,代码未执行,所以 ifreturn 不会影响预编译
  • 全局上下文 预编译 ,GO:全局上下文对象
    • 1,寻找变量声明
    • 2,寻找函数声明
    • 3,代码顺序执行
var a = 10;
function a(){}
console.log(a)

全局上下文预编译 变量提升 GO
1,找变量
	a = undefined
2,找函数声明
	a = function
3,代码顺序执行
  • 非运算符
    • !!'',空字符为假,三假为假
    • !!' ',空格字符为真,两假抵消,还是真
  • 函数是一种对象类型
  • 函数的length属性,形参个数
  • 函数的name属性,函数名
  • 函数的prototype属性,原型

3,作用域链

  • [[scope]]作用域
  • 函数创建时生成的,js内部的一个隐式属性
  • 存储函数作用域链的容器
    • 存储 AO,函数执行期上下文
    • 存储 GO,全局执行期上下文
  • AO 是一个即时容器,在函数执行完后,会被销毁
function a(){
	function b(){
		var b =2
	}
	var a = 1
	b()
}
var c = 3
a()
  • a函数声明
    在这里插入图片描述

  • a函数执行
    在这里插入图片描述

  • b函数声明,b声明时b的作用域链 = a执行时a的作用域链
    在这里插入图片描述

  • b函数执行
    在这里插入图片描述

  • b函数执行结束,b结束时b的作用域链 = b定义时b的作用域链
    在这里插入图片描述

  • a函数执行结束,a的AO被销毁,a的作用域链回到了a定义时的状态

  • b存在于a的AO中,a的AO不存在了,所以b和b自身的作用域都不存在了
    在这里插入图片描述

  • js文件执行,预编译,生产 GO,函数a声明

  • 函数a声明时

    • 生成自身的 [[scope]] 作用域
    • [[scope]]作用域存储函数自身的作用域链
    • GO外层函数的AO存入自己的作用域链
  • 函数a执行时(前一刻)

    • 生成自己的 AO
    • 将函数自身的 AO 存储在作用域链的最顶端。第0位。
    • 外层函数的 AOGO 依次向下排列
    • 查找变量时,在作用域链中,自上向下,依次查找,所以函数只能从自身访问外层作用域的变量
  • 函数被声明时的作用域链,和外层函数的执行时作用域链相同

    • 内函数声明 .scope chain = 外函数执行 .scope chain
  • 函数a执行结束

    • 自身 AO 从自身作用域链中销毁
    • 自身作用域链中,剩下 外层函数的AOGO
  • 外层函数执行结束

    • 外层函数的 AO 从自身作用域链中被销毁
    • 函数a声明在外层函数的 AO ,所以函数a和函数a的作用域不存在了
  • 外层环境执行,内层作用域生成;外层环境结束,内层作用域销毁

function a(){
	function b(){
		function c(){}
	}
	b()
}
a()

// a定义 ->  a.[[scope]] -> 0 : GO
// a执行 ->	 a.[[scope]] -> 0 : a -> AO
// 							1 : GO
// a执行时,b定义
// b定义 ->  b.[[scope]] -> 0 : a -> AO
//							1 : GO
// b执行 ->  b.[[scope]] -> 0 : b -> AO
//							1 : a -> AO
//							2 : GO
// b执行时,c定义
// c定义 ->  c.[[scope]] -> 0 : b -> AO
//							1 : a -> AO
//							2 : GO
// c执行 ->  c.[[scope]] -> 0 : c -> AO
//							1 : b -> AO
//							2 : a -> AO
//							3 : GO
// c结束 ->  c.[[scope]] -> 0 : b -> AO
//							1 : a -> AO
//							2 : GO
// b结束 ->  b.[[scope]] -> 0 : a -> AO
//							1 : GO
// 			 c.[[scope]] -> 伴随b -> AO 被销毁
// a结束 ->  a.[[scope]] -> 0 : GO
// 			 b.[[scope]] -> 伴随a -> AO 被销毁

4,闭包

  • 当内部函数被作为函数返回值返回时,一定会形成闭包
  • 闭包会造成原来执行结束的函数的作用域链不被释放
  • 过度的闭包可能造成内存泄露(常驻内存),或加载过慢
  • 闭包可以看成一种现象
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 外层函数将内层函数作为返回值返回出去
  • 外层函数执行结束时,会从自己的作用域链中移除自身 AO
  • 但是,被返回出去的内层函数的作用域链中,保存了外层函数的 AO,可以访问并修改外层函数的 AO
  • 内层函数被保存到外部变量中,执行前一刻会生成自身的 AO,执行结束,销毁自身 AO ,但作用域链中仍保存着外层函数 AOGO
  • 一个函数a返回多个函数b、c时,形成闭包
    • 这些被返回的函数b、c在被声明时,引用的外层环境 AO 是同一个 AO,是a自身执行前一刻生成的 AO
function a(){
	var d = 10
	function b(){
		d++
		console.log(d)
	}
	function c(){
		d--
		console.log(d)
	}
	return [b,c]
}
let fns = a()
fns[0]()	// 11
fns[1]()	// 10
// fns[0] 就是函数b,fns[1] 就是函数c,它们在声明时,引用了同一个外层环境AO,就是函数a执行前一刻生成的AO
function test(){
	var arr = []
	for(var i = 0;i<10;i++){	// 结束时,i=10,不满足条件,结束
		arr[i] = function (){
			console.log(i)
		}
	}
	return arr 		// 返回出去10个函数,形成闭包
}
var arr = test() 	// arr是一个数组,有10个函数
for(var j = 0;j<10;j++){
	arr[j]()		// 因为test函数结束时的i=10,所以这10个函数打印的i都是10 ,test函数的AO中i=10 
}
  • 函数不return形成闭包
    • 不return怎么形成闭包?
    • 闭包本质,函数内部的函数被保存在全局变量,导致执行完的外部函数AO不被销毁
    • 也就是说,只要能把函数内部的函数保存到全局即可
function fn(){
	let a = 0
	function fn2(){
		a++
		console.log(a)
	}
	window.fn3 = fn2
}
fn()
fn3()	// console.log(1)
fn3()	// console.log(2)
fn3()	// console.log(3)

let fn3;
function fn(){
	let a = 0
	function fn2(){
		a++
		console.log(a)
	}
	fn3 = fn2
}
fn()
fn3()	//1
fn3()	//2
fn3()	//3

4-1,闭包产生——私有变量

如下:

  • 调用parent,返回child函数,parent执行结束
  • 被返回的child函数,作用域链上保留了parent的AO对象,所以能访问到num变量
  • 因为result保留了child函数,所以只有result能访问到num
  • 因此,num是result的私有变量
function parent(){
	var num = 0;
	function child(){
		num++
		console.log(num)
	}
	return child
}
// 此时,只有parent的返回值函数,能够访问到parent作用域链上的num变量,即:num是result的私有变量
let result = parent()

5,IIFE 立即执行函数

  • 一种不需要调用,会自己立即执行的函数
  • 可以用来作为 初始化函数
  • 普通函数会一直存在于 GO 中,随时随地都可以调用,但是 IIFE立即执行后会自动销毁
  • IIFE匿名函数,写了函数名也会被忽略,且无法通过函数名访问(因为执行完就销毁,要名字没用)
  • 写立即执行函数,通常在头部或尾部写一个 ; 分号,表明这是一个表达式,不容易出错
  • 两种写法:
// 方式1 (函数)(参数)
;(function (args){
	console.log(args)
})(args)

// 方式2 W3C建议 (函数(参数))
;(function (args){
	console.log(args)
}(args))
  • 什么时候可以直接加 () 执行?
    • () 括号是一个执行符号
    • () 括号包住的 都是表达式,可以跟 () 执行符号执行
    • var fn = function (){} ,这是一个赋值表达式,可以直接跟 () 执行符号执行
    • function fn(){},函数声明不是表达式,不能用 () 执行符号执行
      • 将函数声明变成表达式的方法:前面加+、-、!、&&、||
    • 立即执行函数 IIFE 是一个表达式
上面闭包里面打印的并不是1-10,因为i已经变为10了
我们可以立即执行打印当时的i
function test(){
	var arr = []
	for(var i = 0;i<10;i++){	// 结束时,i=10,不满足条件,结束
		(function (){
			console.log(i)
		})()
	}
}
也可以用立即执行函数,将当时的i保存起来
function test(){
	var arr = []
	for(var i = 0;i<10;i++){
		(function (j){	// 因为这里时立即执行函数,所以arr立即保存了一个函数 ,且打印的时具体数字
			arr[j] = function (){
				console.log(j)	// 立即执行函数传进来一个数字i,所以这里的j是具体的数字,而不是形参
			}
		})(i)
	}
	return arr
}
var arr = test()
for(var n = 0; n<10; n++){
	arr[n]()	// 打印 0-9
}
  • 函数表达式忽略函数名
// 函数表达式声明函数,忽略函数名
var fn1 =  function test1(){console.log(1)}
console.log(test1)  // 引用错误 test1 is not defined

// 括号内的都会变为表达式,所以下面其实是一个匿名函数
(function test2(){console.log(2)})
console.log(test2)  // 引用错误 test2 is not defined

// 作为条件判断时,括号内函数声明会变为表达式,值为真,但表达式会忽略函数名
if(function fn3(){}){
	console.log("函数声明作为判断条件为真") // 函数声明作为判断条件为真
	console.log(fn3)	// 引用错误 fn3 is not defined
}

6,报错类型

  • ReferenceError引用错误,访问的目标对象不存在
    • 变量或函数未被声明
      • fn(),调用不存在的函数
      • console.log(a),访问不存在变量
    • 无效赋值
      • var a = 1 = 2; ,给1赋值为2,无效赋值invalid left-hand side in assignment
      • console.log(1) = 2,无效赋值
  • SyntaxError语法错误
    • 命名不规范
      • var 1 = 1; ,变量名不能用数字开头
      • var 1abc = abc;
      • function 1fn(){},函数名不能用数字开头
      • var new = 1;,不能用关键字命名
      • function = 1
  • RangeError范围错误
    • arr.length = -5,数组长度赋值为负数
    • 对象方法参数超出可行范围
      • num.toFixed(-5),保留小数位数为负数
  • TypeError,类型错误,例如把一个字符串当作函数调用
    • 123() ,123是一个数字,不是函数
    • obj.say(),调用对象不存在的方法,它会认为这个方法是一个属性,不是函数
    • new ‘string’,new 后面必须是一个构造器,函数
  • URIErrorURI错误
    • 通常是encodeURIdecodeURI时的错误
  • Error和上面对应的错误,都有对应的构造器,我们可以人为抛出错误;
  • 错误通常是系统自动抛出
  • JS是单线程,一行代码出错,下面的无法继续执行
  • try catch,可以帮助我们处理错误,帮助代码在出错时,不影响其他代码执行
    • try,尝试执行
    • catch,捕获 try 的错误,try没有错误不触发catch
    • finally,无论try是否发生错误,catch是否被触发,finally最后都会被执行,类似于与try catch 无关的东西,和写在try catch 外部一样
    • throw,定义并抛出错误
var num = 10;
try{
	if(num !== 10){
		throw new RangeError("num应该等于10") // 定义范围错误并抛出
	}
	console.log(num)	// 正常执行
	console.log(str)	// 错误,str is not defined
	console.log(true)	// 错误后面不会执行
}catch(e){
	console.log(e)		// 错误类型:错误信息
	console.log(e.name)	// 错误类型
	console.log(e.message)	// 错误信息
}finally{
	console.log("finally 不受try catch影响,一定会执行")	// 一定执行
}

7,对象

  • 删除属性或方法 delete obj.propdelete obj.methods
  • 创建对象的方式有:
    • 字面量let obj = {a:1}
    • 系统自带的构造函数 let obj = new Object(),和字面量创建的对象没有区别
    • 自定义构造函数
  • 自定义构造函数:
function GetObj(name,gender,age){
	this.name = name;
	this.gender = gender;
	this.age = age;
}
let person = new GetObj('zxf','boy','9')
  • 对象内部的 this 指向对象自身
  • 构造函数没有执行时,没有自己的 AO ,所以压根没有 this ,实例化时,this 指向 实例对象
  • 构造函数的使用方式:
以前使用:
function GetObj(name,gender,age){
	this.name = name;
	this.gender = gender;
	this.age = age;
}
// 这种方式看上去更简单,但是当构造函数过于复杂时,我们根本不知道传进的值作用是什么
let obj = new GetObj('zxf','boy','9')

高级使用方式:
function GetObj(obj){
	this.name = obj.name;
	this.gender = obj.gender;
	this.age = obj.age;
}
// 这种方式看上去好像麻烦,甚至多此一举,但是可以帮助我们更方便的使用复杂的构造函数
let obj = new GetObj({name:'zxf',gender:'boy',age:'9'})

例如:vue就是采用这种方式!!!
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})
  • 构造函数在 new 实例化的时候,做了什么?
    • 在函数内部 创建了一个空对象 this
    • 为该对象 初始化属性、方法
    • 自动返回该对象
function Person(name,age){
	// new时,自动创建一个对象
	// this = {}
	// 初始化对象属性、方法
	this.name = name; 	// this => {name : name}
	this,age = age;		// this => {name : name , age : age}
	// 自动返回该对象
	// return this
}

把隐式操作变为显式:

  • 隐式 => 显式
  • new 帮我们做的,我们自己来做
  • 如果我们手动返回一个基础数据类型,会被构造函数自动忽略
    • function Fn(){ ... return 123 } ,返回this,构造正常
  • 如果我们手动返回一个引用数据类型,会替代构造函数返回的this
    • function Fn(){ ... return [] } ,构造函数返回一个[]空数组,而不是实例对象
function Person (name,age){
	var that = {}
	that.name = name
	that.age = age
	return that
}
let obj = Person('zxf','28')
console.log(obj)	// {name:'zxf' , age:'28'}
  • 构造函数形成闭包

如下,构造函数的实例对象的方法,能访问到构造函数内部变量num,形成闭包

function Fn(){
	var num = 0;
	this.add = function(){
		num++;
		console.log(num)
	};
	this.sub = function(){
		num--;
		console.log(num)
	}
}
  • 函数返回对象形成闭包

其实和构造函数同理,只不过是以对象代替this

function fn(){
	var num = 0;
	var obj = {
		add:function(){
			num++;
			console.log(num);
		},
		sub:function(){
			num--;
			console.log(num);
		}
	}
	return obj;
}
// obj.add方法、obj.sub方法,能访问到num,形成闭包,num是它俩的私有变量;
let obj = fn()

8,包装类

  • 原始值没有自己的属性和方法
  • 但是经过包装的原始值可以设置属性和方法
    • new Number(1) ,生成一个数字对象,可以设置属性方法,可以作为数字参与运算
    • new String('a'),生产一个字符串对象,可以设置属性方法
    • new Boolean('真'),生成一个布尔值对象,可设置属性方法
    • undefinednull 是无法设置对象和方法的
let str1 = 'abc'
console.log(str1.length)
// 实际上访问的是 new String(str1).length,字符串对象身上是由length属性的

let num = 123
// 实际上访问的时 new Number(num).length,而数字对象身上没有length属性,所以打印的是 undefined
console.log(num.length)

let num2 = new Number(123)
// 实际过程
console.log(num.length)

9,原型、原型链

  • 原型prototype,是function对象身上的一个属性
  • 构造函数实例化对象时, Prototype可以用来定义实例对象的公共属性和方法
  • 每个 实例对象都可以继承prototype原型上的方法和属性
  • 对象自己有就用自己的,对象自身找不到的属性和方法才会去原型上找
  • 固定写死的,方法哦原型上,需要实例化时配置的,放到构造函数内部
  • 实例对象只能访问 prototype 上的属性方法,不能增删改
  • prototypeconstructor属性指向构造函数本身,可以通过修改constructor修改它的原型
  • 每个实例化对象都有一个 _proto_ 属性,指向它构造函数的原型 Prototype
  • 字面量和new Object()生成的对象,__proto__直接指向Object.protoType
  • _proto_ 从哪来?实例对象为什么能通过__proto__访问到构造函数的protoType
之前说到,new 关键字实例化对象时,会自动生成一个 this 对象,并返回出去
function Person (name,age){
	var that = {
	//	这个this对象并不是一个空对象,它有一个_proto_ 属性,保存了构造函数原型,所以实例对象能通过_proto_属性访问构造函数的原型
		__proto__:Person.protoType
	}
	that.name = name
	that.age = age
	return that
}
  • 修改构造函数的 protoType 对象,对实例对象的影响
function Car(obj){
	this.color = obj.color;
	this.price = obj.price;
}
Car.protoType.name = '宾利'let car1 = new Car({color:'白色',price:100})
Car.protoType.name = '奔驰'
console.log(car1)	// 请问这里car1的name是宾利还是奔驰呢?
解析:
let car1 = new Car({color:'白色',price:100})	// 这个时候,发生了什么?
如下:
function Person (obj){
	var this = {
		__proto__:{ name:'宾利' }	// 实例化时,已经将当前的构造函数的protoType属性存在实例对象身上
	}
	this.color = obj.color;
	this.price = obj.price;
	return this
}
// 此时car1已经生成了,再更改构造函数的protoType,只会影响到后来实例对象保存的__proto__,对之前的不影响
Car.protoType.name = '奔驰'
// 所以,car1的name还是宾利

如果我们修改的不是protoType上的原始数据,而是修改protoType上一个引用数据的某个属性呢?
function Car(obj){}
Car.protoType = {name:宾利,num:5,info:{ price:100 }}
let car = new Car() 
通过实例修改原型的引用数据
car.info.price--
car.num--
原型上的基本数据类型不会被改变,但引用数据类型被实例修改时也改变了
console.log(Car.protoType)	// {name:宾利,num:5,info:{ price:99 }}
  • _proto_原型链
    • 每个实例对象身上都有一个 _proto_隐式原型
      • 它身上有一个 constructor 属性指向该实例对象的构造函数
      • 因为 _proto_隐式原型本质上也是一个对象,所以它也有自己的 __proto__ 对象
    • 沿着原型、原型的原型、……去继承原型的属性和方法,这种继承关系就是 原型链
    • 原型链的顶端是 Object.protoType
  • Object.create()
    • 可以用来创建对象,参数可以为 对象 或 null
    • 如果参数是对象,这个对象将作为被创建对象的 原型__proto__
    • 如果参数是 null ,则生成的对象 没有原型 __proto__

10,call、apply 修改this指向

通过call、和apply实现继承
function Person(name,age,color,carName){
	Car.apply(this,[color,carName])
	this.name = name;
	this.age = age;
	this.handle = function(){
		console.log(this.age+"岁的"+this.name+"买了一辆"+this.color+"的"+this.carName)
	}
}
function Car(color,carName){
	this.carName = carName;
	this.color = color;
}
let obj = new Person('zxf',28,'白色','宾利')

11,js圣杯模式继承

现在,有两个构造函数,A 和 B,想让A的实例继承B的原型,可以:

function A(){}
A.prototype = {name:"A"}
function B(){}
B.prototype = new A()

这样,B的实例就可以通过原型,访问A的实例对象,A的实例对象又可以访问A的原型,从而实现,B的实例可以访问A的原型

  • 但是,这样实现的话,每次继承都需要先实例化一次A,且都会继承A实例对象的属性

那我们可以直接将B的原型指向A的原型,这样就会只继承A的原型,而不继承它的实例属性

B.prototype = A.prototype
  • 但是,这样的话,我们修改了B的prototype,会发现,A的prototype也变了
  • 修改B的原型,直接影响到了A的原型
  • 我们可以借助一个第三方构造函数,作为缓冲区
function A(){}
A.prototype = {name:"A"}
function B(){}
function Buffer(){}
Buffer.prototype = A.prototype
B.prototype = new Buffer()
B.prototype.age = 5
// 这样,B的实例对象可以继承自己原型的属性,且不会修改A的原型
let b = new B()

12,圣杯模式继承_包装,并进行模块化

包装圣杯模式继承:

  • 构造函数A继承构造函数B的原型prototype,但并不会继承B的实例属性
  • 且:修改A的原型prototype,不会影响B的原型prototype
  • 核心:利用一个构造函数作为缓冲
// start1_圣杯继承包装
var inherit = (function(){
	var Buffer = function (){}
	return function(parent,child){
		Buffer.prototype = parent.prototype
		// 以Buffer的prototype和实例对象作为缓冲
		child.prototype = new Buffer()
		// 此时,child的prototype的constructor成为了parent,修正它的constructor
		child.prototype.constructor = child
		// 标注它的继承源
		child.prototype.super_calss = parent
	}
})()

// 上面立即执行函数的返回值就是一个继承函数,我们用inherit保存
// 使用时,构造函数child就可以继承构造函数parent的原型,且修改child的prototype不会影响parent
inherit(parent,child)

// start2_模块化
    // init中,是我的模块
    // 下面的立即执行函数立即执行,生成独立模块,但内部代码不会立即执行,而是保存在变量init中,通过init调用执行
    var init_zxf = (function () {
        function Profession() { }
        Profession.prototype = {
            name: "程序员",
            work: "写代码",
            tool: "计算机",
            say: function () {
                console.log("我是一名" + this.stationName + this.name + "," + "我的工作是使用" + this.tool + this.work + "," + "我使用的语言有" + this.lang)
            }
        }

        function Front() { }
        function End() { }
        inherit(Profession, Front)
        inherit(Profession, End)
        Front.prototype.stationName = "前端"
        Front.prototype.lang = "HTML,CSS,Javascript"
        End.prototype.stationName = "后端"
        End.prototype.lang = "Node,Java,SQL"
        return { Front, End }
    })()
    var front = new init_zxf.Front()
    var end = new init_zxf.End()
    front.say()
    end.say()

13,对象访问属性的方式

对象访问属性的方式有两种

  • obj.property
  • obj['property'],类似于PHP的方式
  • 其实,本质上,obj.property的方式,也是通过浏览器包装为obj['property']的方式,但是它会尝试把property转换为字符串
var obj = {num : 10}
for (var key in obj){
	console.log(obj[key])
	// 不能使用obj.key,
	// 因为它会把key变量转成字符串 'key'
	// obj['key'],不成立,obj没有键名为 'key' 的属性
}

13_判断对象属性是否在原型上

  • **obj.hasOwnProperty(key)
  • 判断key属性是不是obj对象原型上的属性**
  • 排除原型上的属性
function Fn(){
	this.name = 'zxf';
}
Fn.prototype.age = 28;
Object.prototype.gender = 'boy'
let obj = new Fn()
for(var key in obj){
	console.log(obj[key])	// name,age,gender的值都会被打印
}

for(var key in obj){
		// obj.hasOwnProperty(key),判断key属性是不是obj对象原型上的属性
	if(obj.hasOwnProperty(key)){
		console.log('不是原型上的:'+obj[key])
	}
}

13_判断属性是否存在于对象中

  • property in obj
  • 本质上就是隐式的 obj[property]
  • in不排除原型上的属性
function Obj(){
	this.name = 'zxf'
}
Obj.prototype.age = 18
let obj = new Obj()
console.log('name' in obj)
console.log(obj['name'])
console.log('age' in obj)
console.log(obj['age'])
console.log('weight' in obj)
console.log(obj['weight'])

13_判断对象的原型链上有没有给定的构造函数

  • A instanceof B
  • 判断A对象的原型链上有没有B构造函数
  • 不能判断基本数据类型
function Fn(){
	this.name = 'xhm';
}
let xhm = new Fn()
console.log(xhm instanceof Fn)		// true
console.log(xhm instanceof Object)	// true
console.log([] instanceof Fn)		// false
console.log({} instanceof Fn)		// false
console.log([] instanceof Object)	// true
console.log({} instanceof Object)	// true
console.log(123 instanceof Number)	// false 不能判断基本数据类型
console.log('abc' instanceof String)// false
console.log(true instanceof Boolean)// false

13_判断数组类型

  • let arr = [1, 2, 3]
  • arr.constructor
  • arr instanceof Array
  • Object.prototype.toString.call(arr)

14,this指向

  • 全局this,指向window
  • 预编译函数,指向window
  • call/apply,改变this指向
  • 构造函数的this,指向实例对象

15,callee / caller

  • callee,指向当前执行的函数,在哪个函数里面,就指向哪个函数,函数内部使用
  • arguments对象身上的一个属性
function fn(){
		// 指向所在的函数自身
	console.log(arguments.callee)
}
fn()	// function fn(){ console.log(arguments.callee) }

// 如果函数是立即执行函数,没有名字,就无法通过名字找到函数,这时就可以通过callee找到
// IIFE递归求和
var sum = (function(n){
	if(n<=1){
		return 1;
	 }
	 return n + arguments.callee(n - 1)
})(10)
console.log(sum)	// 55
  • caller,指向函数的调用者,函数内部使用
  • window调用指向为null
function fn1(){
	fn2()
	console.log(fn1.caller)
}
function fn2(){
	console.log(fn2.caller)
}
fn1()

注意:arguments、callee、caller在严格模式下use stract均无法使用

16,插件与IIFE模块化

16_插件写法

  • 立即执行,挂载到全局
  • IIFE前面加 ; ,形成表达式
;(function(){
	var Test = function(){}
	Test.prototype = {}
	window.Test = Test
})();

16_模块化写法

  • IIFE立即执行,将模块保存在一个变量中
let init = (function(){
	function utils(){}
	return utils
})()
  • 有些工具并不是立即执行的,而是要有触发方式,所以用变量保存起来

17,深浅拷贝

深浅拷贝主要是针对引用数据而言的,因为它们的存储方式是:在栈中存指针,指针指向存在堆中的数据

17_浅拷贝

  • 直接循环赋值
let person1 = {
	name:'zxf',
	age:90,
	gender:'boy',
	habby:['song','jump','rap']
}
let person2 = {}
// person2 = person1 ,直接赋值会造成同一个引用,修改一个,两个都变
for(var key in person1){
	person2[key] = person1[key]
}
// 这样,可以拷贝第一层的数据
// 但是如果第一层有引用数据类型,那还是会造成相同引用,修改第一层的引用类型,另一个对象也会变化,这就是浅拷贝

// 浅拷贝封装
function clone(parent,child){
	var child = child || {};
	for(var key in parent){
		if(parent.hasOwnProperty(key)){
			child[key] = parent[key]
		}
	}
	return child
}

17_深拷贝

  • 递归遍历赋值
  • JSON转换
// 递归遍历
function deepClone(parent,child){
	// 如果传要生成的对象就用,没有就用一个 新对象/新数组 代替
	var child = child || (Array.isArray(parent) ? [] : {}),
		// 保存原型链顶端方法,用于后面检测数据类型
		toStr = Object.prototype.toString,
		// 先保存数组类型的检测值,不是数组,就是对象
		arrType = "[object Array]";
		for(var key in parent){
			// 只拷贝对象自身值,不拷贝原型上的值
			if(parent.hasOwnProperty(key)){
				// 是引用类型,且不是null
				if(typeof(parent[key]) === 'object' && parent[key] !== null){
					// 是数组就创建衣蛾数组接收,并进行深拷贝,是对象,就用对象继续深拷贝
					toStr.call(parent[key]) === arrType ? child[key] = [] : 
													child[key] = {};
					deepClone(parent[key],child[key]);
				}else{
					// 不是引用类型,就直接拷贝
					child[key] = parent[key];
				}
			}
		}
	return child
}
var arr = [1,'xhm', true, null, undefined, [ 666, { name:"deepClone" } ] ]
var obj = {
	name:'iKun', 
	age:8, 
	happy:['song', 'jump', 'rap'], 
	info:{
		dance:2.5,
		ball:'good',
		gender:'boy',
		fans:['boy?','girl']
		}
}
let arr2 = deepClone(arr,[])
let obj2 = deepClone(obj)
console.log(arr2)
console.log(obj2)
// JSON深拷贝
let obj = {
	name:'iKun', 
	age:8, 
	happy:['song', 'jump', 'rap'], 
	info:{
		dance:2.5,
		ball:'good',
		gender:'boy',
		fans:['boy?','girl']
		}
}
let obj2 = JSON.parse(JSON.stringify(obj))
// 缺陷,不能拷贝 函数 和 undefined

18,数组

声明数组的三种方法

  • 字面量,let arr = []
  • 构造函数,let arr = new Array()
  • 构造函数,let arr = Array(),不加new也可以。

数组的构造函数和原型

  • 所有的数组都由 Array() 构造函数生成
  • 数组原型 __proto__ 都指向 Array()prototype

稀松数组

  • 数组并不是每一位都要有值,但是最后一位如果是空,会被自动忽略
  • 有空值的,空值为 empty,属于稀松数组。

实例化数组时的参数

  • new Array()
  • 参数只有一个数字,则是定义数组长度
  • 参数只有一个非数字,则为数组元素
  • 参数是多个,则会成为数组元素
  • 参数是多个时,不准有空值,如new Array(,1,,2,),首尾中间都不能有空值。

数组是对象的另一种形式,底层机制一样

  • 访问数组不存在的元素,为undefined
  • 访问对象不存在的属性,为undefined
  • 数组和对象,访问不存在的键,值都为undefined

数组方法

  • 数组方法都是继承于构造函数的原型Array.prototype
  • push、unshift 都可以一次 添加多个参数。尾部添加、头部添加
  • pop、shift 都没有参数。尾部删除、头部删除
  • reverse反转数组
  • splice剪切数组,参数1,开始位置;参数2,裁剪长度,后面的参数,从开始位置添加的数据。
    • 开始位置可以是负数,从后向前数,最后一个是 -1
  • sort ,数组排序,默认按照ASCII码排序,数字只能按首位排序
    • arr.sort((a,b)=>{return a-b})
    • 返回正值,升序; 返回负值,降序;
  • concat合并数组
  • toString数组转字符串
  • slice截取数组[0,1,2,3,4,5].slice(-3,-1) => [3,4]
    • 不传参数,全部截取并返回
    • 传一个参数,从该位置截取到最后
    • 传两个参数,从开始位置截取到结束位置之前
    • 负数从后向前数,倒数第一位是 -1
  • split字符串转数组
    • 参数1,分隔符
    • 参数2,截取数组长度
  • join,参数是一个字符,将 数组用字符拼接为字符串
arr.sort() 随机排序
// 利用它的返回值,正值升序,负值降序,随机返回正负值即可
arr.sort(()=>{
	// Math.random(),数学方法,返回0~1之间,包含0不包含1的随机数,所以返回值也是正负数随机的
	return Math.random() - 0.5;
})

字符串长度排序
arr.sort((a,b)=>{
	return a.length - b.length;
})

数组方法重写

arr.push()
Array.prototype.push = function (){
	for(var i = 0; i < arguments.length; i++){
		// this就是数组实例,this.length就是添加数组的最后一位
		this[this.length] = arguments[i]
	}
	// push 的返回值是数组长度
	return this.length
}

类数组

  • length 属性
  • 采用下标访问元素
  • arguments 就是一个类数组
let arr_ = { '0':0, '1':1, '2':2, length:3 }
console.log(arr_)		// { '0':0, '1':1, '2':2, length:3 }
console.log(arr_[0])	// 0
console.log(arr_[1])	// 1
console.log(arr_[2])	// 2
console.log(arr_.length)// 3
// 给对象加上数组原型上的splice方法,就可以把它的字面量变为数组[]
arr_.push = Array.prototype.push
console.log(arr_)		// [ '0':0, '1':1, '2':2, length:3, splice:f splice() ]

19,自定义原型方法

  • 数组原型上定义去重方法
Array.prototype.unique = function(){
	var temp = {},
		newArr = [];
	for(var i < 0; i < this.length; i++){
		// 检测新对象身上是否有该属性,没有就添加该属性,并放入新数组
		if(!temp.hasOwnProperty(this[i])){
			temp[this[i]] = this[i];
			newArr.push(this[i]);
		}
	}
	// 返回该新数组
	return newArr;
}
  • 字符串原型上定义去重方法
    • 本质上和数组一样,只是数组是把不重复的元素push进新数组,而字符串是+=拼接进新数组
String.prototype.unique = function(){
	let temp = {},
		newStr = "";
	for(var i = 0; i < this.length; i++){
		if(!temp.hasOwnProperty(this[i])){
			temp[this[i]] = this[i];
			newStr += this[i];
		}
	}
	return newStr
}
  • 封装检测数据类型的方法
function getType(val){
	var type = typeof(val);
	var toStr = Object.prototype.toString;
	// toStr 检测数据的结果对应的值
	var res = {
		'[object Array]':'array',				// 数组
		'[object Object]':'object',				// 对象
		'[object Date]':'date',				// 对象
		'[object RegExp]':'regexp',				// 对象
		'[object Number]':'object number',		// 包装 数字对象
		'[object String]':'object string',		// 包装 字符串对象
		'[object Boolean]':'object boolean',	// 包装 布尔对象
	};
	// null 比较特殊,首先排除
	if(val === null){
		return 'null';
	}else if(type === 'object'){
		// 引用值进一步判断
		var getRes = toStr.call(val);
		return res[getRes];
	}else{
		// 其他的 基本类型 和 函数 直接返回
		return type;
	}
}

20,stract 严格模式

  • 开启严格模式use stract,直接写这个字符串,该作用域该字符下的代码就要遵守严格模式
  • 严格模式规则:
    • 不能使用 with(scope){} 改变作用域,太消耗性能
    • 不能使用 argumentscaller、callee
    • 变量只能给声明过的变量赋值,不能不声明只赋值
    • 严格模式下,函数this不赋值,默认为undefined
    • 函数参数不能重复
    • 对象属性不能重复,但是并不会报错

21,垃圾回收机制

  • JS自带垃圾回收机制
    • 找出不再使用的变量
    • 释放回收其内存空间,从而避免内存泄露
    • 固定的时间间隔运行
  • 变量生命周期
    • 全局变量不讨论,因为直到页面关闭
    • 函数作用域内部变量,到函数执行完成
    • 每次调用函数,函数内部变量都是一个新的变量,因为上个变量的生命周期在函数执行结束时也结束了(闭包例外)
  • 引用计数法:
    • 当一个对象被引用时,其引用计数加一。
    • 当引用被删除或覆盖时,其引用计数减一。
    • 当一个对象的引用计数为0时,表示对象不再被使用,因此可以被回收。
    • 缺点:循环引用
      • 引用计数方法存在循环引用的问题,即两个或多个对象相互引用,导致它们的引用计数永远大于0,尽管它们实际上已经不再使用。
      • 例如:a.prop = b; b.prop = a
  • 标记清除法:
    • 进入环境时,打上标记,代表活动状态(比如在函数内声明)
      • 标记阶段: 变量所用活动对象(可访问的对象),并将它们标记为“活动状态”
    • 离开环境时,清除标记
      • 清除阶段: 遍历所用对象,将未标记为“活动状态”的对象回收,释放其占用的内存空间
    • 定时清除 未标记活动状态的对象 ,释放其占用的内存空间
    • 优点:避免用用计数法的循环引用
    • 缺点:每次需要遍历所有对象,对性能有一点负面影响

22,关于 undefined

  • undefined 既是一个原始数据类型,也是一个原始值数据
  • 它是全局对象上的一个属性, window.undefined
  • 不可写(重写)
    • writable:false
    • window.undefined = 1,错误
  • 不可配置
    • configurable:false
    • delete window.undefined,错误
  • 不可枚举
    • 遍历找不到该属性
  • 不可重新定义
    • 使用Object.defineProperty()无法重新定义该属性
  • 系统会给一个未赋值的变量自动赋值为undefined,类型也是undefined
    • console.log(a) // undefined
    • typeof a // undefined
  • 函数没有返回值时,默认为返回undefined
  • undefined不是JS的关键字和保留字
    • 但是在全局声明undefined不生效,因为window上右undefined属性且无法重写
    • 但是在函数内部可以声明变量名为undefined的变量
  • void(args),返回值固定为undefined,且就是window上的undefined
    • 作用是,过去写代码不规范,可能会声明undefined变量,这时候undefined就不再是常规的undefined,而是一个单独的变量
    • 这个时候,我们想要使用window.undefined,可以使用void(0)来创建 undefined

666,面试题

1,apply指定this为null,则不改变this指向

function fn1(){
	fn2.apply(null,arguments)
}
function fn2(){
	console.log(arguments)
}
fn1(1, 2, 3)
// fn1调用fn2,并修改this指向,传入参数
// 但修改this指向为null,视为不修改指向
// 即fn2(1,2,3)
// 结果打印	1,2,3

2,typeof 可能的返回值

  • number、string、boolean、undefined、object、function

3,ageuments 与 形参的映射

  • 使用形参名修改参数值,arguments 会改变
  • 使用 arguments 改变参数值,形参对应的值也会改变
function fn(a,b,c){
	arguments[2] = 10
	console.log(c)
}
fn(1,2,3)	// 10

function fn2(a,b,c){
	c = 10
	console.log(arguments[2])
}
fn2(1,2,3)	// 10

4,逗号运算符

  • 括号执行表达式内,返回值是最后的东西
var f = (
function f1(){
	return 123
},
function f2(){
	return 'abc'
}
)()
console.log(typeof(f))	// 'string'

5,隐式转换

  • null == undefined,判等为真
  • null === undefined,全等为假
  • isNaN('100')先隐式转换,Number('100'),然后进行isNaN()判断
  • parseInt('1a1') == 1parseInt从头截取是数字的部分为值
  • NaN == NaN,所有值包括NaN自身都不等于NaN
function isnan(val){
	var res = Number(val)
	if(res == NaN){
		return true
	}else{
		return false
	}
}
isnan('abc')
// 考点,NaN == NaN ,

6,对象的引用地址

  • {} == {},空对象不等于空对象,因为地址不同
  • var obj = {}; var obj2 = obj1;,让两个空对象相等

7,类数组

  • 类数组 转数组 let arr = Array.prototype.slice.call(arguments)
  • arguments,类数组,是一个js内置对象,用对象模拟的数组
// 类数组 长度和属性未重叠的类数组
let obj = {
	'0':1,
    '1':2,
	'length':2,
	'splice':Array.prototype.splice,
	'push':Array.prototype.push
	}
obj.push(1)
obj.push(2)
console.log(obj)
// {
//    "0": 1,
//    "1": 2,
//    "2": 3,
//    "3": 4,
//    "4": 1,
//    "5": 2,
//    "length": 6,
//     push:f push(), 
//     splice:f splice()
// }

// 长度和属性重叠的类数组
var obj = {
	'2':3,
	'3':4,
	'length':2,
	'splice':Array.prototype.splice,
	'push':Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)	// ???
// 要搞懂这个,需要知道push的原理,push对类数组做了什么?
function (){
	for(var i = 0; i < arguments.length; i++){
		// this就是数组实例,this.length就是添加数组的最后一位
		this[this.length] = arguments[i]
	}
	this.length++
}
// 即通过 arr[arr.length] = 参数 的形式赋值
// 但是 obj对象原本就有 属性2和属性3	
// 本质上就是让属性和length重叠,导致最后没有指向length,而是指向了已有的属性
//	obj.push(1)  ⇒ obj[2] = 1,本质上是将obj对象的属性2的值改为1 ,length++ ==> 3
//	obj.push(2)  ⇒ obj[3] = 2,本质上是将obj对象的属性3的值改为2
// 	obj ==> {
//				empty*2, 
//				2:1, 
//				3:2,
//				length:4,
//				push:f push(),
//				splice:f splice()
//			}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值