JS基础
1,数据类型,隐式转换、循环
- 数字与NAN运算
+-*/%
都是NAN - 数字与字符串数字比较,把字符串转换为数字
- 字符串比较,从左向右,依次比较
ASCII
码- 例如:
'89' > '9'
,比较它们的ASCII
码值
- 例如:
- NAN 与包括自己的任何东西都不相等
==
- 条件互斥的情况,使用
else-if
,最后的else
兜底异常意外 if
处理范围判断,switch
处理固定值- 假值:
undefined、null、NAN、""、0、false
&&
,且运算符,遇真往后走,遇见假值或走到最后,就返回当前值; 找假返回;let a = 1 && 2
,a为2let b = 1 && null && 2
,b为null
||
,或返回值,遇假往后走,遇见真值或走到最后,就返回当前值; 找真返回;let a = null || 0
,a=0let 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=1;
if(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的返回值是字符串
- typeof 得出的
- null 和 undefined
- null 代表空值,
typeof null = 'object'
,是历史遗留问题,为了初始化对象,可以理解为一个bug - undefined ,未定义或为赋值
- undefined隐式转换数字是NAN,null是0
- undefined 和 null 没有
toString()
方法 - null == undefined ,true
- null 代表空值,
- 隐式转换
- 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 影响,因为预编译时,代码未执行,所以 if、return 不会影响预编译
- 全局上下文 预编译 ,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位。 - 外层函数的
AO
和GO
依次向下排列 - 查找变量时,在作用域链中,自上向下,依次查找,所以函数只能从自身访问外层作用域的变量
- 生成自己的
-
函数被声明时的作用域链,和外层函数的执行时作用域链相同
- 内函数声明
.scope chain
= 外函数执行.scope chain
- 内函数声明
-
函数a执行结束
- 自身
AO
从自身作用域链中销毁 - 自身作用域链中,剩下 外层函数的
AO
和GO
- 自身
-
外层函数执行结束
- 外层函数的
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
,但作用域链中仍保存着外层函数AO
和GO
- 一个函数
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 后面必须是一个构造器,函数
URIError
,URI错误- 通常是
encodeURI
和decodeURI
时的错误
- 通常是
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.prop
,delete 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('真')
,生成一个布尔值对象,可设置属性方法undefined
和null
是无法设置对象和方法的
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
上的属性方法,不能增删改 prototype
的constructor
属性指向构造函数本身,可以通过修改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){}
改变作用域,太消耗性能 - 不能使用
arguments
的caller、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)
// undefinedtypeof 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') == 1
,parseInt
从头截取是数字的部分为值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()
// }