内置对象
JS 中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object)。
基本类型有六种: null,undefined,boolean,number,string,symbol。
按存储类型分为值类型,引用类型
js内置函数:Object、Array、Boolean、Number、String、Function、Date、RegExp、Error
Typeof
typeof 对于基本类型,除了 null 都可以显示正确的类型
//值类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 没有声明,但是还会显示 undefined
//typeof 对于对象,除了函数都会显示 object。引用类型~~
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
typeof null // 'object'
PS:为什么会出现这种情况呢?因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object。
instanceof
instanceof用于判断引用类型属于哪个构造函数。
//var a={}其实是var a=new Object()的语法糖
//var a=[]其实是var a=new Array()的语法糖
//function Foo(){}其实是var Foo=new Function()的语法糖
[] instanceof Array //true
instanceof的内部机制是通过判断对象的原型链中是不是能找到对象的prototype。
//规则就是left.__proto__是不是强等于right.prototype,不等于再找left.__proto__.__proto__直到为null
function instanceof(left, right) {
// 获得类型的原型
let prototype = right.prototype
// 获得对象的原型
left = left.__proto__
// 判断对象的类型是否等于类型的原型
while (true) {
if (left === null) return false
//这里重点:严格===
if (prototype === left) return true
left = left.__proto__
}
}
Object.prototype.toString.call()
判断一个变量的正确类型,Object.prototype.toString.call(xx)。返回 [object Type] 的字符串。
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(“abc”);// "[object String]"
Object.prototype.toString.call(123);// "[object Number]"
Object.prototype.toString.call(true);// "[object Boolean]"
Object.prototype.toString.call(fn); // "[object Function]"
Object.prototype.toString.call(new Date()); // "[object Date]"
Object.prototype.toString.call([]); // "[object Array]"
类型转换
转Boolean
在条件判断时,除了 undefined, null, false, NaN, '', 0, -0,其他所有值都转为 true,包括所有对象。
对象转基本类型
对象在转换基本类型时,首先会调用 valueOf 然后调用 toString。并且这两个方法你是可以重写的。
let a = {
valueOf() {
return 0
}
}
当然你也可以重写 Symbol.toPrimitive ,该方法在转基本类型时调用优先级最高。
let a = {
valueOf() {
return 0;
},
toString() {
return '1';
},
[Symbol.toPrimitive]() {
return 2;
}
}
1 + a // => 3
'1' + a // => '12'
[] == ![] -> true解析
// [] 转成 true,然后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true
PS:何时使用===和==?
if(obj.a == null){
//jq源码推荐的写法
//这里相当于obj.a===obj.a||obj.a===undefined
}
原型
- 所有引用类型(数组、对象、函数)都具有对象属性,即可自由扩展属性(null除外)
- 所有引用类型(数组、对象、函数),都有一个proto属性,值为一个普通对象
- 所有函数都有一个prototype属性,值为一个普通对象
- 寻找一个对象的某个属性,如果这个对象本身没有这个属性。那么就去它的proto(即它的构造函数的prototype)中寻找
var obj={};obj.a=100;
obj.__proto__===Object.prototype //true
function Foo(name){
this.name=name
}
Foo.prototype.consoleName = function(){
console.log(this.name)
}
var f = new Foo('lisi');
f.printName = function(){
console.log(this.name)
}
f.printName()//lisi
f.consoleName()//lisi
f.toString()//"[object,object]"要去f.__proto__.__proto__中找
循环对象本身的属性
for(item in obj){
//高级浏览器已经在for in中屏蔽的自身原型的属性,为保证程序的健壮性
if(obj.hasOwnProperty(item)){
console.log(item)
}
}
new
- 描述new一个对象的过程
- 新生成了一个对象
- 链接到原型(this指向这个对象)
- 绑定 this
- 返回新对象(返回this)
- 实现new
//方法1
function _new(fn, ...arg) {
const obj = Object.create(fn.prototype);
const ret = fn.apply(obj, arg);
return ret instanceof Object ? ret : obj;
}
//方法2
function create() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}
- new的优先级
function Foo() {
return this;
}
Foo.getName = function () {
console.log('1');
};
Foo.prototype.getName = function () {
console.log('2');
};
new Foo.getName(); // -> 1
new Foo().getName(); // -> 2
//当单于
new (Foo.getName());
(new Foo()).getName();
PS:new Foo() 的优先级大于 new Foo,先执行了 Foo.getName() ,所以结果为 1;对于后者来说,先执行 new Foo() 产生了一个实例,然后通过原型链找到了 Foo 上的 getName 函数,所以结果为 2
执行上下文
- 全局执行上下文
- 函数执行上下文
- eval 执行上下文
每个执行上下文中都有三个重要的属性
- 变量对象(VO),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
- 作用域链(JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
- this
var a = 10//全局上下文
function foo(i) {
var b = 20 //函数foo上下文
}
foo()
b() // call b second
function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
PS:ES6中引入了let。let不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。
var foo = 1
(function foo() {
foo = 10
console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }
PS:当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到 foo,但是这个值又是只读的,所以对它的赋值并不生效,所以打印的结果还是这个函数,并且外部的值也没有发生更改
this
this要在执行时才能确定值,定义时无法确定
var a = {
name:'a',
fn:function(){
console.log(this.name)
}
}
a.fn() //this===a
a.fn.call({name:'b'})//this==={name:'b}
var fn1 = a.fn
fn1()//this===window
闭包
闭包:函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。
function A() {
let a = 1
function B() {
console.log(a)
}
return B
}
面试题
for ( var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );//5个6
}, i*1000 );
}
方法1
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
方法2
for ( var i=1; i<=5; i++) {
setTimeout( function timer(j) {
console.log( j );
}, i*1000, i);
}
方法3
for ( let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
对于 let 来说,他会创建一个块级作用域,相当于
{ // 形成块级作用域
let i = 0
{
let ii = i
setTimeout( function timer() {
console.log( ii );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}
实际开发中闭包的应用
- setTimeout传递的第一个函数不能带参数,通过闭包可以实现传参效果。
function f1(a) {
function f2() {
console.log(a);
}
return f2;
}
var fun = f1(1);
setTimeout(fun,1000);//一秒之后打印出1
定义行为,然后把它关联到某个用户事件上(点击或者按键)。代码通常会作为一个回调(事件触发时调用的函数)绑定到事件。
函数防抖:由于还需要一个变量来保存计时,考虑维护全局纯净,可以借助闭包来实现
封装私有变量,收敛权限——函数柯里化
function f1() {
var sum = 0;
var obj = {
inc:function () {
sum++;
return sum;
}
};
return obj;
}
let result = f1();
console.log(result.inc());//1
console.log(result.inc());//2
console.log(result.inc());//3
function f1() {
var sum = 0;
function f2() {
sum++;
return f2;
}
f2.valueOf = function () {
return sum;
};
f2.toString = function () {
return sum+'';
};
return f2;
}
//执行函数f1,返回的是函数f2
console.log(+f1());//0
console.log(+f1()())//1
console.log(+f1()()())//2
PS:所有js数据类型都拥有valueOf和toString这两个方法,null除外。
valueOf()方法:返回指定对象的原始值。
toString()方法:返回对象的字符串表示。
在数值运算中,优先调用了valueOf,字符串运算中,优先调用toString
深浅拷贝
浅拷贝
- Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。注意:Object.assign()拷贝的是属性值,假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值
//当拷贝的源对象的属性值是一个对象时,拷贝的只是对象的引用值,因此当修改属性值的时候两个对象的属性值都会发生更新
var a = {a : 'old', b : { c : 'old'}}
var b = Object.assign({}, a)
b.a = 'new'
b.b.c = 'new'
console.log(a) // { a: 'old', b: { c: 'new' } }
console.log(b) // { a: 'new', b: { c: 'new' } }
Array.prototype.slice()方法提取并返回一个新的数组,如果源数组中的元素是个对象的引用,slice会拷贝这个对象的引用到新的数组
Array.prototype.concat()用于合并多个数组,并返回一个新的数组。
var arr1 = [{a: 'old'}, 'b', 'c']
var arr2 = [{b: 'old'}, 'd', 'e']
var arr3 = arr1.concat(arr2)
- 展开运算符(…)来解决
let a = {
age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // 1
深拷贝
- 通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
但是该方法存在有局限性:
- 会忽略 undefined
- 会忽略 symbol
- 不能序列化函数
- 不能解决循环引用的对象
- 通过jQuery的extend方法实现深拷贝
var array = [1,2,3,4];
var newArray = $.extend(true,[],array); // true为深拷贝,false为浅拷贝
- lodash函数库实现深拷
let result = _.cloneDeep(test)
- Reflect法
// 代理法
function deepClone(obj) {
if (!isObject(obj)) {
throw new Error('obj 不是一个对象!')
}
let isArray = Array.isArray(obj)
let cloneObj = isArray ? [...obj] : { ...obj }
Reflect.ownKeys(cloneObj).forEach(key => {
cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
})
return cloneObj
}
- 手写一个深拷贝
function deepCopy(o) {
if (o instanceof Array) {
let n = [];
for (let i = 0; i < o.length; ++i) {
n[i] = deepCopy(o[i]);
}
return n;
} else if (o instanceof Object) {
let n = {}
for (let i in o) {
n[i] = deepCopy(o[i]);
}
return n;
} else {
return o;
}
},
防抖
触发高频事件后n秒只会执行一次,如果n秒内高频事件再次触发,则重新计算时间。(滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作,防页面卡顿)
//思路:每次触发事件时都取消之前的延时调用
// func是用户传入需要防抖的函数,wait是等待时间
const debounce = (func, wait = 50) => {
let timer = null;// 缓存一个定时器id
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
function searchInputResult(){
console.log("防抖成功")
}
var searchInput = documentById('searchInput');
searchInput.addEventListener('input',debounce(searchInputResult))
// 不难看出如果用户调用该函数的间隔小于wait的情况下,上一次的时间还未到就被清除了,并不会执行函数
- 例如在搜索引擎搜索问题的时候,我们当然是希望用户输入完最后一个字才调用查询接口,这个时候适用==延迟执行==的防抖函数,它总是在一连串(间隔小于wait的)函数触发之后调用。
- 例如用户给interviewMap点star的时候,我们希望用户点第一下的时候就去调用接口,并且成功之后改变star按钮的样子,用户就可以立马得到反馈是否star成功了,这个情况适用==立即执行==的防抖函数,它总是在第一次调用,并且下一次调用必须与前一次调用的时间间隔大于wait才会触发。
一般的防抖会有immediate选项,表示是否立即调用
// 这个是用来获取当前时间戳的
function now() {
return +new Date()
}
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args
// 延迟执行函数
const later = () => setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null
// 延迟执行的情况下,函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 这里返回的函数是每次实际调用的函数
return function(...params) {
// 如果没有创建延迟执行函数(later),就创建一个
if (!timer) {
timer = later()
// 如果是立即执行,调用函数
// 否则缓存参数和调用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer)
timer = later()
}
}
}
节流
高频事件触发,但在n秒内只会执行一次,所有节流会稀释函数的执行频率
防抖和节流的作用都是防止函数多次调用。
//思路:每次触发事件时都判断当前是否有等待执行的延时函数
function throttle(fn){
let canRun = true;//通过闭包保存一个标记
return function(){
if(!canRun) return;//在函数开头判断标记是否true
canRun = false;
//将外部传入的执行函数放在setTimeout中
setTimeout(()=>{
fn.apply(this,argument);
//在setTimeout执行完成后再把标记设为true,表示可以执行下一次循环
canRun = true;
},500)
}
}
function searchInputResult(){
console.log("防抖成功")
}
var searchInput = documentById('searchInput');
searchInput.addEventListener('input',throttle(searchInputResult))
// 节流throttle代码(定时器):
var throttle = function(func, delay) {
var timer = null;
return function() {
var context = this;
var args = arguments;
if (!timer) {
timer = setTimeout(function() {
func.apply(context, args);
timer = null;
}, delay);
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
// 节流throttle代码(时间戳+定时器):
var throttle = function(func, delay) {
var timer = null;
var startTime = Date.now();
return function() {
var curTime = Date.now();
var remaining = delay - (curTime - startTime);
var context = this;
var args = arguments;
clearTimeout(timer);
if (remaining <= 0) {
func.apply(context, args);
startTime = Date.now();
} else {
timer = setTimeout(func, remaining);
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
ps:在节流函数内部使用开始时间startTime、当前时间curTime与delay来计算剩余时间remaining,当remaining<=0时表示该执行事件处理函数了(保证了第一次触发事件就能立即执行事件处理函数和每隔delay时间执行一次事件处理函数)。如果还没到时间的话就设定在remaining时间后再触发 (保证了最后一次触发事件后还能再执行一次事件处理函数)。当然在remaining这段时间中如果又一次触发事件,那么会取消当前的计时器,并重新计算一个remaining来判断当前状态。
/**
* underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {object} options 如果想忽略开始函数的的调用,传入{leading: false}。
* 如果想忽略结尾函数的调用,传入{trailing: false}
* 两者不能共存,否则函数不能执行
* @return {function} 返回客户调用函数
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的时间戳
var previous = 0;
// 如果 options 没传则设为空对象
if (!options) options = {};
// 定时器回调函数
var later = function() {
// 如果设置了 leading,就将 previous 设为 0
// 用于下面函数的第一个 if 判断
previous = options.leading === false ? 0 : _.now();
// 置空一是为了防止内存泄漏,二是为了下面的定时器判断
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 获得当前时间戳
var now = _.now();
// 首次进入前者肯定为 true
// 如果需要第一次不执行函数
// 就将上次时间戳设为当前的
// 这样在接下来计算 remaining 的值时会大于0
if (!previous && options.leading === false) previous = now;
// 计算剩余时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果当前调用已经大于上次调用时间 + wait
// 或者用户手动调了时间
// 如果设置了 trailing,只会进入这个条件
// 如果没有设置 leading,那么第一次会进入这个条件
// 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
// 其实还是会进入的,因为定时器的延时
// 并不是准确的时间,很可能你设置了2秒
// 但是他需要2.2秒才触发,这时候就会进入这个条件
if (remaining <= 0 || remaining > wait) {
// 如果存在定时器就清理掉否则会调用二次回调
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
// 没有的话就开启一个定时器
// 并且不能不能同时设置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};
函数防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
PS:==防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的情况下只会调用一次,而节流的 情况会每隔一定时间(参数wait)调用函数。==
继承
/*********************父类*****************/
function Animal (name) {
this.name = name || 'Animal';
this.colors = ["red", "blue", "green"];
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
- 原型链继承
核心 将父类的实例作为子类的原型
特点:继承了父类的模板,又继承了父类的原型对象
缺点:
1.要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
2.无法实现多继承
3.来自原型对象的引用属性是所有实例共享的
4.创建子类实例时,无法向父类构造函数传参
推荐指数:★★(3、4两大致命缺陷)
function Animal (name) {
this.name = name || 'Animal';
this.colors = ["red", "blue", "green"];
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
function Cat(){}
//核心:父类的实例作为子类的原型,执行了2步操作
//1:新创建的对象复制了父类构造函数内的所有属性、方法
//2:将原型__proto__指向父类原型对象
Cat.prototype = new Animal();
//要想为子类添加新属性或者方法,必须在new Animal(父类)之后
Cat.prototype.name = 'cat';
var cat = new Cat();
- 构造继承
核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
特点:
解决了1中,子类实例共享父类引用属性的问题
创建子类实例时,可以向父类传递参数
可以实现多继承(call多个父类对象)
缺点:
1.实例并不是父类的实例,只是子类的实例
2.只能继承父类的实例属性和方法,不能继承原型属性/方法
3.无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
推荐指数:★★(缺点3))
function Animal (name) {
this.name = name || 'Animal';
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
var cat = new Cat();
- 组合继承
核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
特点:
1.弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
2.既是子类的实例,也是父类的实例
3.不存在引用属性共享问题
4.可传参
5.函数可复用
缺点:
调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
推荐指数:★★★★(仅仅多消耗了一点内存)
function Animal (name) {
this.name = name || 'Animal';
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; //组合继承需要修复构造函数指向
var cat = new Cat();
- 寄生组合继承
通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
// 父类
function SuperType (name) {
this.colors = ["red", "blue", "green"];
this.name = name; // 父类属性
}
SuperType.prototype.sayName = function () { // 父类原型方法
return this.name;
};
// 子类
function SubType (name, subName) {
// 调用 SuperType 构造函数
SuperType.call(this, name); // ----第二次调用 SuperType,继承实例属性----
this.subName = subName;
};
// ----第一次调用 SuperType,继承原型属性----
SubType.prototype = Object.create(SuperType.prototype)
SubType.prototype.constructor = SubType;
let instance = new SubType('An', 'sisterAn')
//闭包实现
(function(){
// 创建一个没有实例方法的类
var Super = function(){};
Super.prototype = Animal.prototype;
//将实例作为子类的原型
Cat.prototype = new Super();
})();
var cat = new Cat();
- 实例继承
核心:为父类实例添加新特性,作为子类实例返回
特点:
不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果
缺点:
实例是父类的实例,不是子类的实例
不支持多继承
function Cat(name){
var instance = new Animal();
instance.name = name || 'Tom';
return instance;
}
var cat = new Cat();
- 拷贝继承
支持多继承
缺点:
1.效率较低,内存占用高(因为要拷贝父类的属性)
2.无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
推荐指数:★(缺点1)
function Cat(name){
var animal = new Animal();
for(var p in animal){
Cat.prototype[p] = animal[p];
}
Cat.prototype.name = name || 'Tom';
}
var cat = new Cat();
- 多继承
// 父类 SuperType
function SuperType () {}
// 父类 OtherSuperType
function OtherSuperType () {}
// 多继承子类
function AnotherType () {
SuperType.call(this) // 继承 SuperType 的实例属性和方法
OtherSuperType.call(this) // 继承 OtherSuperType 的实例属性和方法
}
// 继承一个类
AnotherType.prototype = Object.create(SuperType.prototype);
// 使用 Object.assign 混合其它
Object.assign(AnotherType.prototype, OtherSuperType.prototype);
// Object.assign 会把 OtherSuperType 原型上的函数拷贝到 AnotherType 原型上,使 AnotherType 的所有实例都可用 OtherSuperType 的方法
// 重新指定 constructor
AnotherType.prototype.constructor = AnotherType;
AnotherType.prototype.myMethod = function() {
// do a thing
};
let instance = new AnotherType()
- ES5 中,我们可以使用如下方式解决继承的问题
function Super() {}
Super.prototype.getNumber = function() {
return 1
}
function Sub() {}
let s = new Sub()
//实现思路就是将子类的原型设置为父类的原型
Sub.prototype = Object.create(Super.prototype, {
constructor: {
value: Sub,
enumerable: false,
writable: true,
configurable: true
}
})
- ES6 中,我们可以通过 class 语法轻松解决这个问题
class MyDate extends Date {
test() {
return this.getTime()
}
}
let myDate = new MyDate()
myDate.test() //报错,this is not a Date Object
- extends实现
function _inherits(subType, superType) {
// 创建对象,Object.create 创建父类原型的一个副本
// 增强对象,弥补因重写原型而失去的默认的 constructor 属性
// 指定对象,将新创建的对象赋值给子类的原型 subType.prototype
subType.prototype = Object.create(superType && superType.prototype, {
constructor: { // 重写 constructor
value: subType,
enumerable: false,
writable: true,
configurable: true
}
});
if (superType) {
Object.setPrototypeOf
? Object.setPrototypeOf(subType, superType)
: subType.__proto__ = superType;
}
}
function MyData() {
}
MyData.prototype.test = function () {
return this.getTime()
}
let d = new Date()
Object.setPrototypeOf(d, MyData.prototype)
Object.setPrototypeOf(MyData.prototype, Date.prototype)
ps:因为在 JS 底层有限制,如果不是由 Date 构造出来的实例的话,是不能调用 Date里的函数的。所以这也侧面的说明了:ES6 中的 class 继承与 ES5 中的一般继承写法是不同的
继承实现思路:先创建父类实例 => 改变实例原先的 proto_ 转而连接到子类的 prototype => 子类的 prototype 的 proto 改为父类的 prototype
call、apply、bind
call 和 apply 都是为了解决改变 this 的指向。作用都是相同的,只是传参的方式不同。
除了第一个参数外,call 可以接收一个参数列表,apply 只接受一个参数数组。绑定后会立即执行函数
function add(c,d){
return this.a + this.b + c + d;
}
var s = {a:1, b:2};
console.log(add.call(s,3,4)); // 1+2+3+4 = 10
console.log(add.apply(s,[5,6])); // 1+2+5+6 = 14
add.bind(s,5,3)//不在返回
add.bind(s,3,4)()//1+2+3+4 = 10
模拟实现
思路:不传入第一个参数,那么默认为 window
改变了 this 指向,让新的对象可以执行该函数。那么思路是否可以变成给新的对象添加一个函数,然后在执行完以后删除?
- call
Function.prototype.myCall = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var context = context || window
// 给 context 添加一个属性
context.fn = this
// 将 context 后面的参数取出来
var args = [...arguments].slice(1)
var result = context.fn(...args)
// 删除 fn
delete context.fn
return result
}
- apply
Function.prototype.myApply = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var context = context || window
context.fn = this
var result
// 需要判断是否存储第二个参数
// 如果存在,就将第二个参数展开
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
- bind和其他两个方法作用也是一致的,只是该方法会返回一个函数。并且我们可以通过 bind 实现柯里化
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
//函数作为构造函数用 new 关键字调用时,不应该改变其 this 指向,因为 new绑定 的优先级高于 显示绑定 和 硬绑定
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
字符串常用方法
- concat():连接字符串
'b'.concat('a')//'ba'
[1,2].concat(4,5)//[1,2,4,5]
- indexOf(),lastindexOf():找到匹配项返回索引值(一定是绝对位置),如果没找到返回-1
'javaScript'.indexOf('v')//2
'javaScript'.indexOf('Script')//4
'javaScript'.indexOf('key')//-1
- charAt:返回指定索引位置的字符,若索引越界,返回空字符串。
'javaScript'.charAt(1)//'a'
'javaScript'.charAt(1000)//''
'javaScript'.charAt(-1)//''
- substr(fromIndex,length)从起始索引fromIndex开始截取长度length的字符串
'abc'.substr(1,1)//'b'
'abc'.substr(1)//'bc'
// 从倒数第6个开始截取一个
'javaScript'.substr(-6,1)//'S'
- substring(startIndex,endIndex)
截取 起始索引startIndex 到 结束索引endIndex的子字符串,
结果包含startIndex处的字符,不包含endIndex处的字符。
'javaScript'.substring(1,3)//'av'
//如果省略一个数,则自动获取后面所有
'javaScript'.substring(4)//'Script'
//startIndex或endIndex为负,则为0处理
'javaScript'.substring(-1,1)//'j'
//startIndex=endIndex,则为空字符串
'javaScript'.substring(1,1)//''
//startIndex>endIndex,则为空字符串
'javaScript'.substring(3,1)//=>'javaScript'.substring(1,3)
- slice(startIndex,endIndex)
截取 起始索引startIndex 到 结束索引endIndex的子字符串,
结果包含startIndex处的字符,不包含endIndex处的字符。
//基本用法同substring,不同点:slice可以对数组操作,substring不行
//如果 start < 0 则 start = length(数组长度)+start
'javaScript'.slice(-1,3)
//如果end < 0 则 end= end+length
//end < start 不截取
- split()分割:按给定字符串分割,返回分割后的多个字符串组成的字符串数组。
'a,bc,d'.split(',')//['a','bc','d']
- join()合并:使用您选择的分隔符将一个数组合并为一个字符串
['s','d'].join(',')//'s,d'
- 大小写
'JavaScript'.toLowerCase()//'javascript'
'JavaScript'.toUpperCase()//'JAVASCRIPT'
- replace(exp,replacement)
//用replacement 替换前面正则匹配的值
var str="Visit Microsoft!"
document.write(str.replace(/Microsoft/, "W3School"))//Visit W3School!
//当2个参数是函数
var str = 'please make heath your first proprity';
str = str.replace(/\b\w+\b/g, function(word) {
return word[0].toUpperCase() + word.slice(1);
});//字符串中所有单词的首字母都转换为大写:
数组方法
常用方法
- shift:删除原数组的第一项,返回删除元素的值;如果数组为空则返回undefined
var arr = [1, 2, 3, 4, 5];
var out = arr.shift();
console.log(arr); //[2,3,4,5]
console.log(out); //1
var arr = [];
var out = arr.shift();
console.log(arr); //[]
console.log(out); //undefined
- unshift:将参数添加到原数组开头,返回数组的长度(ie6下返回undefined)
var arr = [1, 2];
var out = arr.unshift(-1, 0);
console.log(arr); //[-1,0,1,2]
console.log(out); //4
- pop:删除原数组的最后一项,返回数组删除的值;如果数组为空则返回undefined
var arr = [1, 2, 3, 4, 5];
var out = arr.pop();
console.log(arr); //[1,2,3,4]
console.log(out); //5
var arr = [];
var out = arr.pop();
console.log(arr); //[]
console.log(out); //undefined
- push:将参数添加到原数组的末尾,返回数组的长度
var arr = [1, 2, 3];
var out = arr.push(4, 5, 6);
console.log(arr); //[1,2,3,4,5,6]
console.log(out); //6
- concat:返回一个将参数添加到原数组中构成的新数组
var arr = [1, 2, 3];
var out = arr.concat(4, 5);
console.log(arr); //[1,2,3]
console.log(out); //[1,2,3,4,5]
- splice(start,deleteCount,val1,val2,...):从start位置开始删除原数组deleteCount项,并从该位置起插入val1,val2,...,返回删除的项组,原数组为删除之后的新数组
var arr = [1, 2, 3, 4, 5];
console.log(arr.splice(2, 2))//[3,4]
console.log(arr.splice(2)); //[3,4,5]
console.log(arr.splice(2, 2, 7, 8, 9, 10))//[3,4],原数组arr:[1,2,7,8,9,10,5]
- reverse() 将数组反转,返回值是反转后的数组
let arr = [1,2,3,4,5]
console.log(arr.reverse()) // [5,4,3,2,1]
console.log(arr) // [5,4,3,2,1]
- sort(sortby)为升序排列,但是先调用每个数组项的toString()方法,然后比较字符串来排序,是按ASCII进行比较的;
参数sortby 可选,用来规定排序的顺序,但必须是函数。
//快速扰乱数组排序
var arr = [1,2,3,4,5,6,7,8,9,10];
arr.sort(function(){
return Math.random() - 0.5;
})
console.log(arr);
var arr = [0, 1, 5, 10, 15];
arr.sort();
console.log(arr);//0,1,10,15,5,注意这里是字符串按ASCII进行比较的
//调用这样的函数就按数值方式排列了
function sortNumber(a, b) {
return a - b
}
arr.sort(sortNumber);
console.log(arr); //0,1,5,10,15
- slice(start,end):返回从原数组中指定start到end(不包含该元素)之间的项组成的新数组,如只有一个参数,则从start到数组末尾
let arr = [1,2,3,4,5]
console.log(arr.slice(1,3)) // [2,3]
console.log(arr) // [1,2,3,4,5]
- split() 将字符串转化为数组
let str = '123456'
console.log(str.split('')) // ["1", "2", "3", "4", "5", "6"]
- join(separator):将数组的元素组成一个字符串,以separator为分隔符,省略的话则用默认用逗号为分隔符
var arr = [1, 2, 3, 4, 5, 6];
console.log(arr); //[1, 2, 3, 4, 5, 6]
console.log(arr.join()); //1,2,3,4,5,6
- arr.forEach(item,index,arr),
- arr.map(item,index,arr),return 返回一个新数组
经典面试题
//parseInt(string, radix),string 必需。要被解析的字符串。
//radix 可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。
parseInt("10"); //返回 10
parseInt("19",10); //返回 19 (10+9)
parseInt("11",2); //返回 3 (2+1)
parseInt("17",8); //返回 15 (8+7)
parseInt("1f",16); //返回 31 (16+15)
parseInt("010"); //未定:返回 10 或 8
['1','2','3'].map(parseInt)
// parseInt('1', 0) -> 1
// parseInt('2', 1) -> NaN
// parseInt('3', 2) -> NaN
ps: arr.forEach()和arr.map()的区别
1. arr.forEach()是和for循环一样,是代替for。arr.map()是修改数组其中的数据,并返回新的数据。
2. arr.forEach() 没有return arr.map() 有return
- arr.filter(item,index)
let arr = [1,2,3,4,5]
let arr1 = arr.filter( (value, index) => value<3)
console.log(arr1) // [1, 2]
- arr.every() 依据判断条件,数组的元素是否全满足,若满足则返回ture
let arr = [1,2,3,4,5]
let arr1 = arr.every( (value, index) =>value<3)
console.log(arr1) // false
let arr2 = arr.every( (value, index) =>value<6)
console.log(arr2) // true
- arr.some() 依据判断条件,数组的元素是否有一个满足,若有一个满足则返回ture
let arr = [1,2,3,4,5]
let arr1 = arr.some( (value, index) =>value<3)
console.log(arr1) // true
let arr2 = arr.some( (value, index) =>value>6)
console.log(arr2) // false
- arr.reduce(callback, initialValue)
callback(Accumulator,currentValue,index,srcArray):
Accumulator必选上一次调用回调返回的值。提供的初始值(initialValue)就衡等于initialValue 不提供默认为数组的第一个元素
currentValue 必选 --数组中当前被处理的数组项
index 可选 --当前数组项在数组中的索引值
srcArray 可选 --源数组
let arr = [0,1,2,3,4]
let arr1 = arr.reduce((preValue, curValue) =>
//0+0,0+1,1+2,3+3,6+4
preValue + curValue
)
console.log(arr1) // 10
//5+0,5+1,6+2,8+3,11+4
let arr2 = arr.reduce((preValue,curValue)=>preValue + curValue,5)
console.log(arr2) // 15
- arr.reduceRight(callback, initialValue) 与arr.reduce()功能一样,不同的是,reduceRight()从数组的末尾向前将数组中的数组项做累加
- arr.indexOf() 查找某个元素的索引值,若有重复的,则返回第一个查到的索引值若不存在,则返回 -1
let arr = [1,2,3,4,5,2]
let arr1 = arr.indexOf(2)
console.log(arr1) // 1
let arr2 = arr.indexOf(9)
console.log(arr2) // -1
- arr.lastIndexOf() 和arr.indexOf()的功能一样,不同的是从后往前查找
es6数组方法
- Array.from() 将伪数组变成数组,就是只要有length的就可以转成数组
let str = '12345'
console.log(Array.from(str)) // ["1", "2", "3", "4", "5"]
let obj = {0:'a',1:'b',length:2}
console.log(Array.from(obj)) // ["a", "b"]
- Array.of() 将一组值转换成数组,类似于声明数组
let str = '11'
console.log(Array.of(str)) // ['11']
//等价于 console.log(new Array('11')) // ['11]
//但是new Array()有缺点,就是参数问题引起的重载
console.log(new Array(2)) //[empty × 2] 是个空数组
console.log(Array.of(2)) // [2]
- arr.copyWithin(target,start,end) 在当前数组内部,将制定位置的数组复制到其他位置,会覆盖原数组项,返回当前数组
//target --必选 索引从该位置开始替换数组项
//start --可选 索引从该位置开始读取数组项,默认为0.如果为负值,则从右往左读。
//end --可选 索引到该位置停止读取的数组项,默认是Array.length,如果是负值,表示倒数
let arr = [1,2,3,4,5,6,7]
let arr1 = arr.copyWithin(1)
console.log(arr1) // [1, 1, 2, 3, 4, 5, 6]
let arr2 = arr.copyWithin(1,2)
console.log(arr2) // [1, 3, 4, 5, 6, 7, 7]
let arr3 = arr.copyWithin(1,2,4)
console.log(arr3) // [1, 3, 4, 4, 5, 6, 7]
- arr.find(callback) 找到第一个符合条件的数组成员
let arr = [1,2,3,4,5,2,4]
let arr1 = arr.find((value, index, array) =>value > 2)
console.log(arr1) // 3
- arr.findIndex(callback) 找到第一个符合条件的数组成员的索引值
- arr.fill(target, start, end) 使用给定的值,填充一个数组,ps:填充完后会改变原数组
//target -- 待填充的元素
//start -- 开始填充的位置-索引
//end -- 终止填充的位置-索引(不包括该位置)
let arr = [1,2,3,4,5]
let arr1 = arr.fill(5)
console.log(arr1) // [5, 5, 5, 5, 5]
console.log(arr) // [5, 5, 5, 5, 5]
let arr2 = arr.fill(5,2)
console.log(arr2)
let arr3 = arr.fill(5,1,3)
console.log(arr3
- arr.includes() 判断数中是否包含给定的值
let arr = [1,2,3,4,5]
let arr1 = arr.includes(2)
console.log(arr1) // ture
let arr2 = arr.includes(9)
console.log(arr2) // false
let arr3 = [1,2,3,NaN].includes(NaN)
console.log(arr3) // true
ps:与indexOf()的区别:
- indexOf()返回的是数值,而includes()返回的是布尔值
- indexOf() 不能判断NaN,返回为-1 ,includes()则可以判断
- arr.keys() 遍历数组的键名
let arr = [1,2,3,4]
let arr2 = arr.keys()
for (let key of arr2) {
console.log(key); // 0,1,2,3
}
- arr.values() 遍历数组键值
let arr = [1,2,3,4]
let arr1 = arr.values()
for (let val of arr1) {
console.log(val); // 1,2,3,4
}
- arr.entries() 遍历数组的键名和键值,返回迭代数组
let arr = [1,2,3,4]
let arr1 = arr.entries()
for (let e of arr1) {
console.log(e); // [0,1] [1,2] [2,3] [3,4]
}
伪数组
伪数组也称类数组。像arguments 或者 获取一组元素返回的集合都是伪数组。
//声明一个空数组,通过遍历伪数组把它们重新添加到新的数组中
var aLi = document.querySelectorAll('li');
var arr = [];
for (var i = 0; i < aLi.length; i++) {
arr[arr.length] = aLi[i]
}
//使用数组的slice()方法 它返回的是数组,使用call或者apply指向伪数组
var arr = Array.prototype.slice.call(aLi);
//原型继承
var aLi = document.querySelectorAll('li');
console.log(aLi.constructor === Array) //false
aLi.__proto__ = Array.prototype;
console.log(aLi.constructor === Array) //true
//ES6中数组的新方法 from()
function test(){
var arg = Array.from(arguments);
arg.push(5);
console.log(arg);//1,2,3,4,5
}
test(1,2,3,4);
reduce的高级用法
- 计算数组中每个元素出现的次数
let names = ['peter', 'tom', 'mary', 'bob', 'tom','peter'];
let nameNum = names.reduce((pre,cur)=>{
if(cur in pre){
pre[cur]++
}else{
pre[cur] = 1
}
return pre
},{})
console.log(nameNum); //{ peter: 2, tom: 2, mary: 1, bob: 1 }
- 数组去重
let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
if(!pre.includes(cur)){
return pre.concat(cur)
}else{
return pre
}
},[])
console.log(newArr);// [1, 2, 3, 4]
- 将多维数组转化为一维
let arr = [[0, 1], [2, 3], [4,[5,6,7]]]
const newArr = function(arr){
return arr.reduce((pre,cur)=>pre.concat(Array.isArray(cur)?newArr(cur):cur),[])
}
console.log(newArr(arr)); //[0, 1, 2, 3, 4, 5, 6, 7]
const flattenDeep = (arr) => Array.isArray(arr)
? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
: [arr]
flattenDeep([1, [[2], [3, [4]], 5]])
模拟实现slice()
Array.prototype.slice = function(start,end){
var result = [];
start = start || 0;
end = end || this.length;
for (var i= start; i<end; i++){
result.push(this[i])
}
return result;
}
为什么0.1+0.2!=0.3
因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。
我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为
// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)
0.2 = 2^-3 * 1.10011(0011)
所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004
parseFloat((0.1 + 0.2).toFixed(10))//0.3
正则表达式
元字符 | 作用 |
---|---|
. | 匹配任意字符除了换行符和回车符 |
[] | 匹配方括号内的任意字符。比如 [0-9] 就可以用来匹配任意数字 |
^ | ^9,这样使用代表匹配以 9 开头。[^9],这样使用代表不匹配方括号内除了 9 的字符 |
{1, 3} | 匹配 1 到 3 位字符 |
(abc) | 只匹配和 abc 相同字符串 |
\ | 转义 |
* | 只匹配出现 0 次及以上 * 前的字符 |
+ | 只匹配出现 1 次及以上 + 前的字符 |
? | ? 之前字符可选 |
| 匹配 | 前后任意字符
修饰语 | 作用 |
---|---|
i | 忽略大小写 |
g | 全局搜索 |
m | 多行 |
字符简写 | 作用 |
---|---|
\w | 匹配字母数字或下划线 |
\W | |
\s | 匹配任意的空白符 |
\S | |
\d | 匹配数字 |
\D | |
\b | 匹配单词的开始或结束 |
\B |