1.闭包
1.1.作用域 & 作用域链
作用域
全局作用域
函数作用域
- 特例: 形参变量也是函数内的局部变量;
- 强调: 只有函数的{},才能形成作用域;
- JS 中没有块级作用域,除了函数{}之外其余的{},都不是作用域;
作用域链
- 函数的作用域链,是在定义时就被定死了;
- 特殊:给从未声明过的变量赋值
i=10;
,会自动在全局创建变量 i; - 变量提升,只能将声明提升,而且不能超出 function;
总结
- JS 中只有两种局部变量:
- 函数内 var 出来的变量;
- 函数的形参变量;
- 看不见 var,形参里面也没有,就不是局部变量。
1.2.作用域的本质
- JS 中,作用域和作用域链都是对象结构;
- 全局作用域,是一个叫 window 的对象结构;所有全局变量和全局函数都是 window 对象的成员;
- 函数作用域也是一个对象;
函数作用域
- 是 JS 引擎在调用函数时临时创建的一个作用域对象(但是没有对象名);
- 保存着函数内所有的局部变量;
- 当函数调用完,函数作用域对象就被释放了。
JS 中没有堆和栈的概念;JS 内存当中,只有关联数组;JS 中,一切皆关联数组。
- 所以,JS 中函数作用域对象,还有个别名——活动的对象(Actived Object),简称 AO;所以,局部变量不可重用;
1.3.闭包
- 管理压岁钱
// 不安全版
let total = 1000;
function pay(money) {
total -= money;
console.log("还剩:", total);
}
pay(100);
total = 0;
pay(100);
// 900
// -100
// 安全版
function manage() {
let total = 1000;
return function (money) {
total -= money;
console.log("还剩:", total);
};
}
let pay = manage();
pay(100);
total = 0;
pay(100);
// 900
// 800
- 闭包:既可重用变量,又可保护变量不被污染的一种编程方法;
闭包的使用,三步骤
- 用外层函数包裹要保护的
变量
和使用变量的内层函数
; - 在外层函数内部
返回内层函数
对象; -
调用外层函数
,用变量接住
返回的内层函数
对象。
什么是闭包
- 闭包就是每次调用外层函数时,临时创建的函数作用域对象;
- 因为被内层函数对象的作用域链引用着,无法释放。
一句话概括闭包是如何形成的
- 外层函数调用后,外层函数的作用域对象,被返回的内层函数的作用域链引用着,无法释放,就形成了闭包对象。
- 闭包缺点:内存泄漏!
- 解决:及时释放不用的闭包——
将保存内层函数的对象变量赋值为null
,使函数变量名和内层函数对象分开;
1.4.闭包答题技巧
外层函数返回内层函数的三种方法:
- return;
- 强行赋值为全局变量;
- 将函数包裹在对象或数组中返回;
多个内层函数,共用变量
function fun() {
var i = 999;
nadd = function () {
i++;
};
return function () {
console.log(i);
};
}
var getN = fun();
getN();
nadd();
getN();
// 999
// 1000
- 相当于三个内层函数,共用变量 i
function fun() {
arr = [];
for (var i = 0; i < 3; i++) {
arr[i] = function () {
console.log(i);
};
}
}
fun();
arr[0]();
arr[1]();
arr[2]();
// 3
// 3
// 3
多次调用外层函数,变量独享,互不影响
function mother() {
var i = 0;
return function () {
i++;
console.log(i);
};
}
var get1 = mother();
get1();
var get2 = mother();
get2();
get1();
get2();
// 1
// 1
// 2
// 2
1.5.常见闭包题目
对象不是作用域
var lilei = {
sname: "lilei",
init: function () {
console.log("this is ", this.sname);
},
study() {
console.log("好好学习");
},
play: () => {
console.log("我要玩游戏: ", this.sname);
},
};
lilei.init();
lilei.study();
lilei.play();
// this is lilei
// 好好学习
// 我要玩游戏: undefined
函数作用域
var a = 10;
function fun() {
var a = 100;
a++;
console.log(a);
}
fun();
console.log(a);
// 101
// 10
function fun() {
a = 100;
a++;
console.log(a);
}
fun();
console.log(a);
// 101
// 101
var a = 10;
function fun(a) {
a++;
console.log(a);
}
fun(a);
console.log(a);
// 11
// 10
var obj = {
name: "yjw",
age: 18,
};
function fun1(obj) {
obj.age = 16;
console.log(obj.age);
}
fun1(obj);
console.log(obj.age);
// 16
// 16
动态生成 4*4 的表格
- 动态生成 4*4 表格,每个表格中有坐标(0,0)-(3,3),点击每个格增加次数,且每个格互不干扰,打印点击次数
方案一:直接给每个格子添加单击事件处理函数,每个处理函数来自于一个闭包的外层函数调用的返回值,且闭包保存一个变量 n,记录当前格子的点击次数。缺点:每个 n 完全隔离,每个格子的 n 和 n 之间无法进行计算等操作。
let container = document.querySelector(".container");
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
const div = document.createElement("div");
div.innerText = `${i},${j}`;
container.appendChild(div);
div.onclick = (function () {
let n = 0;
return function () {
console.log(++n);
};
})();
}
}
方案二:今后只要看到二维的布局,2048 或消消乐,都应该用二维数组来存储所有格子的值。每个按钮的单击事件处理函数中应该只保存自己对应的元素的下标位置!当点击时,通过自己保存的行号和列号来找到二维数组中自己对应的元素值,修改。
let arr = Array(4)
.fill("")
.map(() => Array(4).fill(0));
let container = document.querySelector(".container");
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
const div = document.createElement("div");
div.innerText = `${i},${j}`;
container.appendChild(div);
div.onclick = function () {
console.log(++arr[i][j]);
};
}
}
方案三:上述方案中 arr 是全局变量,容易作弊。用匿名函数自调包裹整段代码!arr 就成了局部变量,会被内层函数引用着,不会释放。但是外部也不能擅自修改了!
(function () {
let arr = Array(4)
.fill("")
.map(() => Array(4).fill(0));
let container = document.querySelector(".container");
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
const div = document.createElement("div");
div.innerText = `${i},${j}`;
container.appendChild(div);
div.onclick = function () {
console.log(++arr[i][j]);
};
}
}
})();
self_this
var name = "window";
var p = {
name: "Perter",
getName: function () {
var self = this;
return function () {
return self.name;
};
},
};
var getName = p.getName();
var _name = getName();
console.log(_name);
// Perter
fun(n,o)
function fun(n, o) {
console.log(o);
return {
fun: function (m) {
return fun(m, n);
},
};
}
var a = fun(0);
a.fun(1);
a.fun(2);
a.fun(3);
// undefined
// 0
// 0
// 0
var b = fun(0).fun(1).fun(2).fun(3);
// undefined
// 0
// 1
// 2
var c = fun(0).fun(1);
c.fun(2);
c.fun(3);
// undefined
// 0
// 1
// 1
btns
-
页面上五个完全相同的按钮,点击某一个按钮,让按钮弹出自己是第几个(ES5 和 ES6 实现)
<div> <button class="btn">click me</button> <button class="btn">click me</button> <button class="btn">click me</button> <button class="btn">click me</button> <button class="btn">click me</button> </div> ... const btns = document.getElementsByClassName('btn'); // ES5 写法 for(var i=0; i<btns.length; i++) { (function(i) { // var i; 形参相当于局部变量 btns[i].onclick = function() { // 每调用一次立即执行函数,就会将传入的值放到局部变量中,供内部专属使用 alert(i); } })(i); // 将外部循环变量i的临时值,赋值给立即执行函数的局部变量,被函数内部永久保存起来 } // ES6 写法 // let 相当于匿名函数自调; // 当 let 碰到 for 循环,会自动生成匿名函数自调,并且会给匿名函数添加参数 i; for(let i=0; i<btns.length; i++) { btns[i].onclick = function() { alert(i); } }
-
衍生:读程序写答案
// ES6 for (let i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 50); } // 0 // 1 // 2
// ES5 for (var i = 0; i < 3; i++) { (function () { setTimeout(() => { console.log(i); }, 50); })(); } // 3 // 3 // 3
// ES5 for (var i = 0; i < 3; i++) { (function (i) { setTimeout(() => { console.log(i); }, 50); })(i); } // 0 // 1 // 2
setTimeout
for (var i = 0; i < 5; i++) {
console.log(i);
}
// 0 1 2 3 4
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 50);
}
// 5 5 5 5 5
for (var i = 0; i < 5; i++) {
// 定时器中的回调函数会存放到“回调函数等待队列”
// for 循环在主程序中执行,只有当主程序中的所有代码都执行完,才能从回调队列中取函数,进入主程序执行
setTimeout(() => {
console.log(i);
}, 0);
}
// 5 5 5 5 5
bind
- bind 的使用
function jisuan1(base, bonus1, bonus2) { console.log(`${this.name}的工资:${base + bonus1 + bonus2}`); } const lilei = { name: "李雷" }; jisuan1.call(lilei, 10000, 300, 500); jisuan1.apply(lilei, [10000, 300, 500]); // bind 返回一个新函数 // 绑定 this const lijisuan = jisuan1.bind(lilei); lijisuan(10000, 5000, 300); // 绑定 this 和参数 const lijisuanbase = jisuan1.bind(lilei, 10000); lijisuanbase(1000, 200);
- bind 为 ES5 中引入的新函数,用原生提供的 call 模拟实现 bind;
// 发现所有的函数都可以调用 bind // 则应该在所有函数的原型对象中添加bind函数 Function.prototype.bind = function (obj) { console.log("自定义 bind:", obj); // 先获得将来调用bind的函数 var fun = this; // 获取 arguments 中除了第一个参数之外的所有实参 // const arr1 = Array.from(arguments).slice(1); const arr1 = Array.prototype.slice.call(arguments, 1); return function () { // 获取当前函数的所有实参值,并将arr1与当前函数实参值合并为一个数组 const arr2 = Array.prototype.slice.call(arguments); const arr = arr1.concat(arr2); fun.apply(obj, arr); }; };
匿名函数
- 匿名函数中的 this 指向 window
var a = 2; var obj = { a: 4, fn1: (function () { this.a *= 2; var a = 3; return function () { this.a *= 2; a *= 3; console.log(a); }; })(), }; var fn1 = obj.fn1; console.log(a); fn1(); obj.fn1(); console.log(a); console.log(obj.a); // 4 // 9 // 27 // 8 // 8
2. 面向对象
2.1.面向对象三大特点
- 封装、集成、多态
2.2.1.封装对象
- 三种方式
1)直接用 {}
- 访问对象中的成员:
- 对象名.属性名;
- 对象名.方法名();
-
{}
:底层实现为new Object()
; -
function
: 底层实现为new Function()
; - 对象方法中,如果想要使用当前对象自己的属性或其它方法时,都要通过 this;
2)new Object()
- 第一步:先创建空对象,
var 对象名 = new Object();
; - 第二步:强行给空对象添加新属性和新方法,
对象名.新属性 = 属性值;
; - js 语言底层最核心的原理:
js中所有对象底层都是关联数组
;- 存储结构:都是键值对的组合;
- 访问成员时:都可以是
对象名/数组['成员名']
或者对象名/数组.成员名
; - 强行给不存在的位置赋值:不会报错,会自动添加该属性;
- 强行访问不存在的位置的值:都返回 undefined,即可以使用
对象.成员名 === undefined
来判断是否包含某个成员; - 都可以用
for in
循环遍历。
- 访问对象属性/数组元素,有三种方式:
-
对象名/数组[已知属性名或者下标名]
; - 如果下标名是非数字的字符串,可以写成
.已知的属性名
; - 如果属性名或者下标来自一个变量,只能用
[变量]
,不能用.
。
-
3)构造函数
- 用 {} 一次只能创建一个对象,如果想创建多个相同结构的对象时,代码就会有很多重复,不便维护。
- 如果想反复创建多个
相同结构
,只是内容不同
的对象时,都用构造函数
。 - 什么是构造函数:描述同一类型的所有对象的
统一结构的函数
。 - 为什么用构造函数:代码重用;
- 使用:两步
- 定义构造函数:
function 类型名(形参1, 形参2, ...) { this.属性名 = 形参1; this.xxx = xxx; this.方法名 = function() {...} }
- 使用构造函数反复创建多个相同结构的对象:
var 对象名 = new 类型名(实参1, 实参2, ...);
new 做了四件事: 1. 创建指定类型的一个新对象; 2. 让子对象继承构造函数的原型对象; 3. 调用构造函数,将 this 替换为新对象,通过强行赋值方式为新对象添加规定的属性; 4. 返回新对象地址。
如果构造函数中,return 一个引用类型的对象,则所有的构造函数规则失效,只返回该引用类型对象。
2.2.2.继承
- 背景:如果将方法定义到构造函数中,那么每次执行 new 时都会默认执行 new Function(),就会反复创建相同函数的多个副本——浪费内存。
- 解决:如果将来发现多个子对象都要使用相同的功能和属性值时,都可以用继承来解决。
- 继承:父对象中的成员,子对象无需重复创建,就可以直接使用,就像使用自己的成员一样!
- 实现:js 中集成都是通过原型对象实现。
原型对象
- 是什么:替所有子对象集中保存共有属性值和方法的父对象;
- 何时使用:今后,只要发现多个子对象都需要使用相同的功能和属性值时,就可将相同的功能和属性值集中定义在原型对象中;
- 如何创建:不用自己创建,而是在定义构造函数时,程序自动附赠我们一个原型对象。
- 构造函数都有一个属性
prototype原型
指向原型对象;new 出来的子对象的__proto__
属性指向构造函数的原型对象;原型对象有一个constructor
属性指向构造函数。 - 如何向原型对象中添加共有属性:只能通过强行赋值
构造函数.prototype.属性/方法= xxx
。
2.2.3.多态
- 同一个函数,在不同情况下表现出不同的状态;
- 包括两种:
- 重载 overload:同一个函数中,输入不同的函数,执行不同的逻辑;——有争议(有说算多态,有说不算多态);
- 重写 override:推翻、遮挡,自己定义重名的进行覆盖。
2.2.4.小结
- 封装:
- 如果只创建一个对象{};
- 如果反复创建多个相同结构的对象:构造函数;
- 继承:如果所有子对象都用相同的属性和方法,我们就使用继承,将相同的属性和方法放到构造函数的原型对象中;
- 多态:如果觉得从父对象中继承过来的成员不好用,就在子对象中重写同名成员。
2.2.this 共有几种情况:8 种
- obj.fun():fun 中的 this -> .前的 obj 对象;
- new Fun():Fun 中的 this -> new 创建的新对象;
- 原型对象中的 this:原型对象中的共有方法,虽然保存在原型对象中,但是却被子对象调用,所以 this 不是指原型对象,而是指将来的某个子对象,
谁调用指谁
;(并不是所有的 this 都指当前函数所在的对象) - 普通函数调用 fun()、匿名函数自调和回调函数中:this 都指 window;严格模式下,都指 undefined。这类函数调用时的特点:前面没有
.
也没有new
! - DOM 事件处理函数中:this 指向当前正在发出事件的.前的 DOM 元素对象。
注:
这里不能改成箭头函数,如果改为箭头函数,这里的 this 就指向外层(可能是 window),功能可能会出错。 - vue:methods 中的方法中的 this 默认都指当前 vue 组件对象;如果想获得当前发出事件的 DOM 元素对象,必须用 $event 关键字和 e.target 联合使用。
- 箭头函数:this 指当前函数之外最近的作用域中的 this;几乎所有的匿名函数都可以用箭头函数简化;箭头函数最大特点:使内外的 this 保持一致;
var lilei = {
sname: "LiLei",
friends: ["涛涛", "楠楠", "动动"],
intro: function () {
this.friends.forEach(function (value) {
console.log(`${this.sname} 认识 ${value}`);
});
},
};
lilei.intro();
// undefined 认识 涛涛
// undefined 认识 楠楠
// undefined 认识 动动
// 解决:把内部的回调函数给成箭头函数
总结:如果函数中没有 this,或者刚好希望函数内的 this 与函数外的 this 保持一致,就可以将普通函数改为箭头函数。
- 不能改为箭头函数的情况:对象中的方法、DOM 事件的处理函数;
- 箭头函数只让 this 指向外部作用域的 this;箭头函数内的局部变量,依然只能在箭头函数内部使用,出了箭头函数不能用。
- 箭头函数底层原理:
bind
。被 bind 绑定的 this,永久不能被替换。
- 替换函数中的 this:
- 临时替换一次:call、apply;
- 永久替换:bind;
/**
* 1、apply 和 call 都是一次性替换,一锤子买卖
* 1.1. call:执行函数、替换函数中的this、传递参数
* 1.2. apply:执行函数、替换this、先拆散数组为多个元素再分别传给函数的形参变量
*
* 2、bind
* 2.1.可以提前永久绑定 this;
* 2.2.可以永久绑定部分实参值;
* 2.3.var 新函数 = 原函数.bind(替换this的对象,不变的实参值)
* 2.4.做三件事:创建一模一样的新函数副本;永久替换this为指定对象;永久替换部分形参变量为固定的实参值。
* 2.5.被bind() 永久绑定的this,即使用call,也无法再替换为其它对象;——箭头函数的底层原理
*/
2.3.创建对象有几种方式:10 种
2.3.1. new Object()
- 缺点:步骤多;
2.3.2. 字面量 {}
- 缺点:如果反复创建多个对象,代码会很冗余;
2.3.3. 工厂函数方式
function createPerson(name, age) {
var o = new Object();
o.name = name;
o.age = age;
return o;
}
var p = createPerson("lilei", 18);
- 缺点:本质还是 Object(),将来无法根据对象的原型对象准确判断对象的类型。
2.3.4. 构造函数
- 先用构造函数定义对象的统一属性结构和方法;
- 再用 new 调用构造函数,反复创建相同属性结构,不同属性值的多个对象;
- 缺点:如果构造函数中包含方法,则重复创建,浪费内存。
function CreatePerson(name, age) {
this.name = name;
this.age = age;
}
var p = new CreatePerson("lilei", 18);
2.3.5. 原型对象
- 先创建完全相同的对象,再给子对象添加个性化属性;
- 缺点:步骤繁琐。
function Person() {}
Person.prototype.name = "王同学";
Person.prototype.age = 18;
Person.prototype.say = function () {
console.log(this.name, "要好好学习!");
};
var p1 = new Person();
p1.name = "张三";
p1.age = 14;
var p2 = new Person();
p2.name = "李四";
p2.age = 16;
var p3 = new Person();
console.log(p1.name, p2.name, p3.name);
// 张三 李四 王同学
2.3.6. 混合模式
- 先创建完全相同的对象,再给子对象添加个性化属性;
- 缺点:不符合面向对象封装的思想。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () {
console.log(this.name, "好好学习");
};
2.3.7. 动态混合
- 先创建完全相同的对象,再给子对象添加个性化属性;
- 缺点:语义不符,其实 if 只在创建第一个对象时有意义。
function Person(name, age) {
this.name = name;
this.age = age;
if (Person.prototype.say === "undefined") {
Person.prototype.say = function () {
console.log(this.name, "好好学习");
};
}
}
2.3.8. 寄生构造函数
- 构造函数里调用其他的构造函数;
- 缺点:可读性差。
function Person(name, age) {
this.name = name;
this.age = age;
if (Person.prototype.say === "undefined") {
Person.prototype.say = function () {
console.log(this.name, "好好学习");
};
}
}
function Student(name, age, className) {
var p = new Person(name, age);
p.className = className;
return p;
}
var p1 = new Student("hanmei", 13, "初一5班");
var p2 = Student("lilei", 14, "初一2班"); // Student 中没有this,所以可以不用new
2.3.9. ES6 Class
- 对比混合模式
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
intro() {
console.log(this.name, "好好学习");
}
}
2.3.10. 闭包:稳妥构造函数
- 不用 this,不用 new!安全可靠;
- 缺点:使用了闭包,容易造成内存泄漏。
function Person(name, age) {
var p = {};
p.getName = function () {
return name;
};
p.setName = function (value) {
name = value;
};
p.getAge = function () {
return age;
};
return p;
}
var p1 = Person("lilei", 12);
console.log(p1.getName(), p1.getAge());
p1.setName("hanmeimei");
console.log(p1.getName(), p1.getAge());
// lilei 12
// hanmeimei 12
2.4.实现继承共有几种方式:7 种
2.4.1. 原型链式继承
- 将父类的实例作为子类的原型
// 定义一个父类型
function Animal(name) {}
// 原型对象方法
Animal.prototype.eat = function (food) {};
function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.name = "cat";
var c = new Cat();
2.4.2. 构造函数继承
// 定义一个父类型
function Animal(name) {}
// 原型对象方法
Animal.prototype.eat = function (food) {};
function Cat(name, age) {
Animal.call(this, name);
this.age = age;
}
var c = new Cat();
2.4.3. 实例继承
// 定义一个父类型
function Animal(name) {}
// 原型对象方法
Animal.prototype.eat = function (food) {};
function Cat(name, age) {
var o = new Animal(name);
o.age = age;
return o;
}
var c = new Cat();
2.4.4. 拷贝继承
- 无法获取父类不可 for in 遍历的方法
// 定义一个父类型
function Animal(name) {}
// 原型对象方法
Animal.prototype.eat = function (food) {};
function Cat(name, age) {
var animal = new Animal(name);
for (var p in animal) {
Cat.prototype[p] = animal[p];
}
this.age = age;
}
var cat = new Cat();
2.4.5. 组合继承
// 定义一个父类型
function Animal(name) {}
// 原型对象方法
Animal.prototype.eat = function (food) {};
function Cat(name, age) {
Animal.call(this, name);
this.age = age;
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var c = new Cat();
2.4.6. 寄生组合继承
// 定义一个父类型
function Animal(name) {}
// 原型对象方法
Animal.prototype.eat = function (food) {};
function Cat(name, age) {
Animal.call(this, name);
this.age = age;
}
(function () {
// 创建一个没有实例方法的类
var Super = function () {};
Super.prototype = Animal.prototype; // 将实例作为子类的原型
Cat.prototype = new Super();
})();
var cat = new Cat();
2.4.7. ES6 class extends 继承
Class 父类型{
constructor() {
...
}
...
}
class 子类型 extends 父类型 {
constructor() {
super();
}
...
}
2.5.实现深克隆有几种方式:2 种
- 拷贝:不是直接赋值
2.5.1.浅克隆
- 只复制对象的第一级属性值。如果对象的第一级属性中又包含引用类型,则只复制地址;
- 问题:如果对象中又包含引用类型的属性值,则导致克隆后,新旧对象依然共用同一个引用类型的对象属性值;任意一方修改了引用类型的对象内容,都会导致另一方同时受影响。
- 通过 for in 实现浅拷贝:
function clone(object) { var o = {}; for (var key in object) { o[key] = object[key]; } return o; }
- 通过 assign 实现浅拷贝。
2.5.2.深克隆
- 不但复制对象的第一级属性值,而且,即使对象中又包含引用类型的属性值,深克隆也会继续复制内嵌类型的属性值。
- 克隆后,两个对象彻底再无瓜葛。
- JSON.stringify()以及 JSON.parse(),无法深克隆值为 undefined 以及函数的属性;
var obj2 = JSON.parse(JSON.stringify(obj));
- 自己实现递归深拷贝
function deepCopy(target) {
let newObj;
if (typeof target === "object") {
if (Array.isArray(target)) {
// 如果是数组
newObj = [];
for (let value of target) {
newObj.push(deepCopy(value));
}
} else if (target.constructor === RegExp) {
// 正则表达式
newObj = target;
} else if (target === null) {
// null
newObj = null;
} else {
// 普通的键值对象
newObj = {};
for (let key in target) {
newObj[key] = deepCopy(target[key]);
}
}
} else {
// 原始数据类型,直接赋值
newObj = target;
}
return newObj;
}
2.6.常见面向对象题目
(1)程序解析:从左往右;程序执行:从右往左
var a = { n: 1 };
var b = a;
a.x = a = { n: 2 };
console.log(a);
console.log(JSON.stringify(b));
a.n = 3;
console.log(a);
console.log(JSON.stringify(b));
// {n: 2}
// {n: 1, x: {n: 2}}
// {n: 3}
// {n: 1, x: {n: 3}}
(2)所有的对象底层都是关联数组,所有的关联数组的属性名/下标都是字符串。如果外界给关联数组添加的下标不是字符串,会隐式调用String()
方法:
var a = {};
var b = { key: "a" };
var c = { key: "c" };
var d = [1, 2];
var e = [3, 4];
a[b] = 123;
a[c] = 456;
a[d] = 789;
a[e] = 101;
console.log(a[b]); // 456
console.log(a[d]); // 789
(3)Foo,函数也是一个对象
function Foo() {
Foo.a = function () {
console.log(1);
};
this.a = function () {
console.log(2);
};
}
Foo.prototype.a = function () {
console.log(3);
};
Foo.a = function () {
console.log(4);
};
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();
// 4
// 2
// 1
(4) that/this
var x = 0;
var foo = {
x: 1,
bar: function () {
console.log(this.x);
var that = this;
return function () {
console.log(this.x);
console.log(that.x);
};
},
};
foo.bar(); // 1
foo.bar()(); // 1 0 1
(5) 构造函数内部,如果 return 一个引用类型的对象,则整个构造函数失效,而是返回这个引用类型的对象。
function A() {}
function B() {
return new A();
}
A.prototype = new A();
B.prototype = new B();
var a = new A();
var b = new B();
console.log(a.__proto__ == b.__proto__); // true
(6)变量提升
- 变量提升:function(){} 整体提前;
var 变量=xxx
中,仅var 变量
提前,=xxx
留在原地; - 任何函数都是普通对象,也可以添加自己的属性,添加的属性和函数体内的部分没有关系;
- 任何函数都可以当做构造函数被 new 调用,且任何函数都有原型对象 prototype 属性,只不过大部分函数不是标准的构造函数内容而已。
function Foo() { getName = function() { console.log(1); } return this; } Foo.getName = function() { console.log(2); } Foo.prototype.getName = function() { console.log(3); } var getName = function() { console.log(4); } function getName() { console.log(5); } Foo.getName(); getName(); Foo().getName(); // Foo() 调用 Foo 函数,只是里面的 this 为 window getName(); // new Foo.getName(); // new 会找到后面的第一个 () new Foo().getName(); new new Foo().getName(); // 2 // 4 // 1 // 1 // 2 // 3 // 3
(7)判断一个对象是不是数组类型,共有几种方法:7 种
- 错误方法:typeof(只能判断基本数据类型 string、number、undefined、boolean,和 function 可以使用,其它的都会返回 object)
var n = 10, str = "hello", b = true, nu = null, un1 = undefined, un2; var f = function () {}; var obj1 = {}, obj2 = [1, 2, 3], obj3 = new Date(), obj4 = /w/; console.log( typeof n, typeof str, typeof b, typeof nu, typeof un1, typeof un2, typeof f, typeof obj1, typeof obj2, typeof obj3, typeof obj4 ); // number string boolean object undefined undefined function object object object object
- 通过爹(原型)来判断:3 种
- obj.proto === Array.prototype —— 不可靠;
var obj1 = {}, obj2 = [1, 2, 3], obj3 = new Date(), obj4 = /w/; console.log(obj1.__proto__ === Array.prototype); console.log(obj2.__proto__ === Array.prototype); console.log(obj3.__proto__ === Array.prototype); console.log(obj3.__proto__ === Array.prototype); // false true false false
obj1.proto = Array.prototype; // 可以改变其指向的父类,导致 上述方法不可靠 2. Object.getPrototypeOf(obj),取代 obj.proto(可能会被浏览器禁用)
var obj1 = {}, obj2 = [1, 2, 3], obj3 = new Date(), obj4 = /w/; console.log(Object.getPrototypeOf(obj1) === Array.prototype); console.log(Object.getPrototypeOf(obj2) === Array.prototype); console.log(Object.getPrototypeOf(obj3) === Array.prototype); console.log(Object.getPrototypeOf(obj4) === Array.prototype); // false true false false
- Array.prototype.isPrototypeOf(obj)
var obj1 = {}, obj2 = [1, 2, 3], obj3 = new Date(), obj4 = /w/; console.log(Array.prototype.isPrototypeOf(obj1)); console.log(Array.prototype.isPrototypeOf(obj2)); console.log(Array.prototype.isPrototypeOf(obj3)); console.log(Array.prototype.isPrototypeOf(obj4)); // false true false false
- 通过妈妈(构造函数)来判断:2 种
- obj.constructor === 妈妈
console.log(obj1.constructor === Object); console.log(obj2.constructor === Array); console.log(obj3.constructor === Date); console.log(obj4.constructor === RegExp); // true true true true
- child instanceof 妈妈,不严谨,不仅会找妈妈,还会向上找妈妈的妈妈
console.log(obj1 instanceof Array); console.log(obj2 instanceof Array); console.log(obj3 instanceof Object); // 这里会寻找妈妈的妈妈 console.log(obj4 instanceof RegExp); // false true true true
- 输出对象中的 DNA,内部隐藏属性 class:1 种
- Object.prototype.toString.call(child) === '[object Array]',最靠谱
// 输出对象中的DNA:内部隐藏属性 class,但是不能通过 .class 访问 console.log(obj1.class, obj2.class, obj3.class, obj4.class); // 都是undefined,只有访问顶级的才有值,而且必须通过 toString 方法才能访问到 // 从原始对象中获取DNA标记,使用顶级父对象中的toString()方法。.call() 可以让任何一个对象,抢到原本不属于它的任何一个函数 console.log(Object.prototype.toString.call(obj1)); console.log(Object.prototype.toString.call(obj2) === "[object Array]"); console.log(Object.prototype.toString.call(obj3)); console.log(Object.prototype.toString.call(obj4)); console.log(Object.prototype.toString.call(3)); // [object Object] true [object Date] [object RegExp] [object Number]
- Object.prototype.toString.call(child) === '[object Array]',最靠谱
- ES5 中新增了一个专门判断一个对象是不是数组的函数:Array.isArray(obj):(是对第六种的封装)1 种
console.log(Array.isArray(obj1)); console.log(Array.isArray(obj2)); console.log(Array.isArray(obj3)); console.log(Array.isArray(obj4)); // false true false false
3.ES6
- let:会自动生成匿名函数自调,如果和for连用,会把每次循环的变量当做实参传给匿名函数9
结构
1.数组解构:下标对下标
var date = [2022, 4, 21];
var [y, m, d] = date;
2.对象解构:属性对属性
var obj = {
name: 'yjw',
age: 18,
hobby() {
console.log('会打篮球');
}
}
var {name, age, hobby} = obj;
console.log(name, age);
hobby();
3.参数解构
- what:在向函数中传参时,将一个大的对象,打散后,传递给形参变量;
- when:多个形参不确定,又要求按顺序传入时;
- why:默认值——只能应对结尾一个参数不确定时;剩余参数——虽然可应对多个参数不确定的情况,但无法规定传入参数的顺序;
// 函数只有最后面一个或多个参数可以不传,否则必须要传 function calc(姓名, 底薪, 房补, 高温, 饭补); calc('lilei', 10000, , 200, 600); calc('hmm', 10000, , , 200); calc('jack', 10000, , 300, 500); // 总结:当多个形参都不确定时,且每个实参值必须对应传给指定的形参变量时,单靠调整形参的个数和顺序,无法满足所有调用的情况 // 解决:参数解构
- how:两步
- 定义形参时,所有的形参变量都要定义在一个对象结构中;
- 调用函数传参时,所有实参值都要放在一个对象结构中,整体传入。
实用小技巧 & JS 常见题目
valueOf、toString
- +f1():+ f1(),会将 f1() 的值转为数字,默认调用 f1 的 valueOf() 函数
function f1() {
var sum = 0;
function f2() {
sum++;
return f2;
}
f2.valueOf = function () {
return sum;
};
f2.toString = function () {
return sum + "";
};
return f2;
}
console.log(Number(f1()));
console.log(+f1()()); // + f1()()
console.log(Number(f1()()()));
// 0
// 1
// 2
- 要求 alert(add(1)),弹出 1;alert(add(1)(2)),弹出 3;alert(add(1)(2)(3)),弹出 6;
- 函数柯里化:可以连续给一个函数反复传参,反复传的参数,还能累计到函数内。
function fun(x) { let sum = x; const add = function (x) { sum += x; return add; }; add.toString = function () { return sum; }; return add; } alert(fun(1)(2)(3)); // 弹出 6
var add = function (x) { var sum = 5; var fun = function (x) { sum = sum + x; return fun; }; fun.toString = function () { return sum; }; return fun; }; alert(add(3)(4)(5)); // 弹出 14
- 获取字符串操作会调用 toString 方法,获取数字操作会调用 valueOf 方法;
- 如果没有 valueOf 方法,获取数字操作也会调用 toString 方法;
- 如果没有 toString 方法,获取字符串操作不会调用 valueOf 方法。
var add = function (x) { var sum = 1; var fun = function (x) { sum = sum + x; return fun; }; fun.toString = function () { // fun的toString() 方法可以返回fun函数闭包中sum变量现在的值 return "string"; }; fun.valueOf = function () { return 100; }; return fun; // 为了可以传入第二个参数,add() 也必须返回函数,才能继续调用,继续传参 }; alert(add(3)(4)(5)); // alert 会自动调用toString console.log("add1: ", Number(add(3)(4)(5))); console.log("add2: ", +add(3)(4)(5)); console.log("add3: ", String(add(3)(4)(5))); console.log("add4: ", add(3)(4)(5)); // 弹出 string // add1: 100 // add2: 100 // add3: string // add4: function
var add = function (x) { var sum = 1; var fun = function (x) { sum = sum + x; return fun; }; return fun; }; alert(add(3)(4)(5)); // 弹出 function
声明和初始化数组
let arr1 = Array(5).fill("a");
console.log(arr1); // (5) ['a', 'a', 'a', 'a', 'a']
let arr2 = Array(5)
.fill("")
.map(() => Array(4).fill("b"));
console.log(arr2);
// (5) [Array(4), Array(4), Array(4), Array(4), Array(4)]
// 0: (4) ['b', 'b', 'b', 'b']
// 1: (4) ['b', 'b', 'b', 'b']
// 2: (4) ['b', 'b', 'b', 'b']
// 3: (4) ['b', 'b', 'b', 'b']
// 4: (4) ['b', 'b', 'b', 'b']
求数组中最大值、最小值,数组求和
const arr = [4, 2, 6, 8, 5, 3, 19, 11];
// 最大值
// const max = Math.max(...arr);
const max = arr.reduce((a, b) => (a > b ? a : b));
// 最小值
// const min = Math.min(...arr);
const min = arr.reduce((a, b) => (a > b ? b : a));
// 求和
const sum = arr.reduce((a, b) => a + b);
console.log(`max: ${max}, min: ${min}, sum: ${sum}`);
// max: 19, min: 2, sum: 58
对象数组排序
const objArr = [
{ name: "lilei", age: 18 },
{ name: "xiaoming", age: 16 },
{ name: "xiaofang", age: 22 },
{ name: "xiaohong", age: 19 },
];
objArr.sort((a, b) => a.age - b.age);
console.log(objArr);
// (4) [{…}, {…}, {…}, {…}]
// {name: 'xiaoming', age: 16}
// {name: 'lilei', age: 18}
// {name: 'xiaohong', age: 19}
// {name: 'xiaofang', age: 22}
过滤掉数组中的 0,false,null,undefined,'', ""
const array = [3, 0, 6, 7, "", false, true, "", "", " "];
array.filter(Boolean);
array.filter((a) => a);
// (5) [3, 6, 7, true, ' ']
少用 if,多用 ||、&&
- 优先级:()、* /、+ -、===、逻辑;
去除重复值
const array = [5, 4, 7, 8, 9, 2, 7, 5];
// const unique = [...new Set(array)];
const unique = array.filter((item, idx, arr) => arr.indexOf(item) === idx);
console.log(unique);
计数器,统计每个字符出现次数
const str = "kafkakfkfioi";
const table = {};
for (let char of str) {
table[char] = table[char] + 1 || 1;
}
console.log(table);
// {k: 4, a: 2, f: 3, i: 2, o: 1}
const str = "kafkakfkfioi";
const tableMap = new Map();
for (let char of str) {
tableMap.set(char, tableMap.get(char) + 1 || 1);
}
console.log(tableMap);
// Map(5) {'k' => 4, 'a' => 2, 'f' => 3, 'i' => 2, 'o' => 1}
可选链 ?.
- 不得不说,这真是新特性中的代码美化神器;
- 如果当前值为 null 或者 undefined,则直接返回 null 或 undefined;
student && student.math && student.math.score;
改写为:
student?.math?.score
空格并 ??
- 顾名思义,只有空时候才会合并
const list11 = null ?? 1;
const list12 = undefined ?? 2;
const list13 = 0 ?? 3;
const list14 = false ?? 4;
const list21 = null || 1;
const list22 = undefined || 2;
const list23 = 0 || 3;
const list24 = false || 4;
console.log(list11, list12, list13, list14);
// 1 2 0 false
console.log(list21, list22, list23, list24);
// 1 2 3 4
将十进制数转化为其它进制
const num = 12;
console.log(num.toString());
console.log(num.toString(2));
console.log(num.toString(8));
console.log(num.toString(16));
// 12
// 1100
// 14
// c
不借助第三个值,交换两个数的值
let a = 5;
let b = 10;
// [a, b] = [b, a];
a = a + b;
b = a - b;
a = a - b;
console.log(a, b);
// 10 5