本文将不断从【排版】到【内容】【与时俱进】不定期优化。面试高频内容放前,低频放后。均以如下形式展开:
- 定义
- 作用
- 原理
- 应用
- 优缺点
- 更新
目录
原型+原型链 Prototype Chain
- 定义
【原型(对象)prototype】原型是 JavaScript 中的一个核心概念,JavaScript 中所有的对象都有一个内置属性,称为它的 原型。原型本身是一个对象,故又称原型对象,可以提供方法和属性,供其他对象继承和使用。
【构造函数 constructor】prototype 对象默认有一个 constructor 属性,指向其关联的构造函数,constructor 属性可以用于创建新的实例。
【function.prototype】构造函数的属性(普通对象没有prototype),指向其原型(对象),修改prototype需要手动修正constructor,否则 constructor 会指向错误的对象。
【.__proto__
】对象的属性(包括所有对象,函数也是对象),指向其构造函数的原型,即创建这个对象的构造函数的 prototype,即let dog = new Animal('Buddy'); dog.__proto__ === Animal.prototype,dog的原型是Animal.prototype
,是实例对象的“继承指针”,用于原型链查找路径,向上查找,原型链终止于null
- 构造函数的prototype属性指向了构造函数原型对象。构造函数的原型对象也是一个对象,也有
__proto__
属性,这样一层一层往上找就形成了原型链。- 实例对象是由构造函数创建的,实例对象的
__proto__
属性指向其构造函数的原型对象。- 构造函数的原型对象的constructor属性指向了构造函数,实例对象的原型的constructor属性也指向了构造函数。
function Animal(name) {
this.name = name;
}
/*
Animal.prototype 是 Animal 构造函数的原型对象,
它默认有一个 constructor 属性,指向 Animal 本身。
*/
console.log(Animal.prototype); // { constructor: ƒ Animal() }
console.log(Animal.prototype.constructor === Animal); // true
console.log(dog.constructor === Animal); // true
console.log(dog.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
Animal.prototype = {
constructor: Animal, // 手动修正 constructor
sayHello: function() {
console.log('Hello');
}
};
let dog = new Animal('Buddy');
console.log(dog.constructor === Animal); // true
//如果没有constructor设置,则得到dog.constructor ===Object
function Parent() {}
Parent.prototype.greet = function() {
console.log("Hello");
};
function Child() {}
Child.prototype = new Parent();
let child = new Child();
console.log(child.__proto__,Child.prototype,child.__proto__ === Child.prototype);
//Child {} Child {} true 都指向new Parent()得到的对象
console.log(Child.prototype.__proto__,Parent.prototype,Child.prototype.__proto__ === Parent.prototype);
//{ greet: [Function (anonymous)] } { greet: [Function (anonymous)] } true
- 作用
实现继承,共享属性和方法。方法只需要在构造函数原型上定义一次,所有它的实例都会通过原型链访问这个方法。节省内存,增加复用。 - 更新
__proto__
已废弃
它的存在和确切行为仅作为遗留特性被标准化,以确保 Web 兼容性,但它存在一些安全问题和隐患。为了更好的支持,请优先使用 Object.getPrototypeOf()/Reflect.getPrototypeOf() 和 Object.setPrototypeOf()/Reflect.setPrototypeOf()。 - 原型链
- 定义:任何对象都有原型对象,但注意普通对象没有prototype,只有构造函数才有。任何原型对象是一个对象,有
__proto__
属性,通过__proto__
一层一层往上找,形成了一条链,就是原型链; - 作用:对象继承其他对象的方法和属性
- 成员的查找机制:当访问一个对象的属性/方法,首先查找它自身有没有该属性。如果没有就查找它的原型(也就是
__proto__
指向的 prototype 原型对象)。如果还没有就查找原型对象的原型(Object的原型对象)。依此类推一直找到 Object 为止(null)。__proto__
指向原型对象,作用是为对象成员查找机制提供一条路线。 - 原型链的性能问题:原型链越长,查找属性的时间越长。
● 如何优化:
○ 避免过深的继承层次。
○ 使用 hasOwnProperty() 判断对象是否有自己的属性。
○ 缓存查找结果,减少重复查找。 - Object.create(null) 创建的对象没有原型、没有继承来的方法属性,是纯净对象
- 定义:任何对象都有原型对象,但注意普通对象没有prototype,只有构造函数才有。任何原型对象是一个对象,有
//手写继承
function Animal(name) {
this.name = name;
}
Animal.prototype.sayHello = function() {
console.log('Hello from Animal');
};
function Dog(name, breed) {
Animal.call(this, name);
/*
在子类的构造函数中,手动调用了父类构造函数 Animal,
并将当前子类实例(this)作为上下文传入,传入name参数
*/
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);//指定原型
//新创建的对象的__proto__直接指向这个参数
Dog.prototype.constructor = Dog;
let dog = new Dog('Buddy', 'Labrador');
dog.sayHello(); // Hello from Animal
- 扩展内置方法
注意:数组和字符串内置对象不能给原型对象覆盖操作 Array.prototype = {} ,只能是 Array.prototype.xxx = function(){} 的方式
Array.prototype.sum = function() {
var sum = 0;
for (var i = 0; i < this.length; i++) {
sum += this[i];
}
return sum;
};
instance原理
- 定义
object instanceof Constructor
返回true/false - 作用
instanceof 用于检查构造函数的 prototype 是否出现在对象的原型链上 - 原理+手写
instanceof 通过遍历 object 的原型链,检查 Constructor.prototype 是否存在于其中。
● 详细步骤:
a. 取 object 的__proto__
(即 Object.getPrototypeOf(object))。
b. 依次向上查找原型链,直到找到 Constructor.prototype,返回 true。
c. 如果原型链查找到 null 仍未匹配,则返回 false。
function myInstanceOf(obj, constructor) {
// 获取 obj 的原型,即 __proto__
let proto = Object.getPrototypeOf(obj);
// 遍历原型链,直到找到匹配的 prototype 或者到达原型链的尽头
while (proto) {
// 如果当前原型与 constructor.prototype 相等,则返回 true
if (proto === constructor.prototype) {
return true;
}
// 继续沿着原型链向上查找
proto = Object.getPrototypeOf(proto);
}
// 如果遍历完整个原型链仍未找到匹配的 prototype,则返回 false
return false;
}
- 缺点+解决
- 仅适用于引用类型, instanceof 不能用于检查基本数据类型,必返回false。因为基本数据类型不是对象,它们没有原型链。
如何判断 null 和 undefined 是否是某个构造函数的实例?都不是,用typeof可以得到
typeof null // "object"(历史遗留bug)
typeof undefined // "undefined"
- 原型链过深会影响性能
- Object.create(null) 创建的对象没有原型和原型链,因此 instanceof 对其无效。
解决:使用 Symbol.hasInstance 自定义 instanceof 逻辑, JavaScript 允许使用 Symbol.hasInstance 修改 instanceof 的行为。
JavaScript 设计了一些 “内置符号(well-known symbols)”,这些符号可以让我们“钩子式”地自定义 JS 行为。[Symbol.hasInstance] 是其中之一(Symbol)
/*
执行 obj instanceof CustomClass 时
其实内部执行的是 CustomClass[Symbol.hasInstance](obj)
*/
class CustomClass {
static [Symbol.hasInstance](instance) {
return instance.customProperty === true;
}
}
let obj = { customProperty: true };//给这个对象手动设置判断自定义原型的属性
console.log(obj instanceof CustomClass); // true
- 对比
1)instanceof 与 constructor 对比:constructor 可能会被修改,从而影响判断。
2)instanceof 与 Object.prototype.toString.call 对比
对于基本数据类型,使用 Object.prototype.toString.call 更加可靠 - 更新
instanceof 在 ES6 class 语法中的应用:ES6 引入了 class 语法, instanceof 的使用obj instanceof Class_name
,但它背后的机制其实还是一样的:检查对象的原型链上是否出现了构造函数的 prototype。
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
console.log(dog instanceof Object); // true
//class 本质上是语法糖
class Person {}
// 等同于
function Person() {}
手写继承
- call()
ES6之前并没有给我们提供 extends 继承。我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承。
call()可以调用函数
call()可以修改this的指向,使用call()的时候 参数一是修改后的this指向,参数2,参数3…使用逗号隔开连接
function fn(x, y) {
console.log(this);
console.log(x + y);
}
var o = {
name: 'andy'
};
fn.call(o, 1, 2);//调用了函数此时的this指向了对象o,输出3
//子构造函数继承父构造函数中的属性
// 1. 父构造函数
function Father(uname, age) {
// this 指向父构造函数的对象实例
this.uname = uname;
this.age = age;
}
// 2 .子构造函数
function Son(uname, age, score) {
// this 指向子构造函数的对象实例
3.使用call方式实现子继承父的属性
Father.call(this, uname, age);
this.score = score;
}
var son = new Son('刘德华', 18, 100);
console.log(son);
//借用原型对象继承方法
// 1. 父构造函数
function Father(uname, age) {
// this 指向父构造函数的对象实例
this.uname = uname;
this.age = age;
}
Father.prototype.money = function() {
console.log(100000);
};
// 2 .子构造函数
function Son(uname, age, score) {
// this 指向子构造函数的对象实例
Father.call(this, uname, age);
this.score = score;
}
// Son.prototype = Father.prototype;
//这样直接赋值会有问题,如果修改了子原型对象,父原型对象也会跟着一起变化
Son.prototype = new Father();
//本质:子类原型对象等于是实例化父类,因为父类实例化之后另外开辟空间,就不会影响原来父类原型对象
// 如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的构造函数
Son.prototype.constructor = Son;
// 这个是子构造函数专门的方法
Son.prototype.exam = function() {
console.log('孩子要考试');
}
var son = new Son('刘德华', 18, 100);
console.log(son);
var arr = [1, 2, 3];
arr ----> Array.prototype ----> Object.prototype ----> null
Array.prototype定义了indexOf()、shift()等方法,因此你可以在所有的Array对象上直接调用这些方法。
函数也是一个对象,它的原型链是:
foo ----> Function.prototype ----> Object.prototype ----> null
Function.prototype定义了apply()等方法,因此,所有函数都可以调用apply()方法
如果原型链很长,那么访问一个对象的属性就会因为花更多的时间查找而变得更慢,因此要注意不要把原型链搞得太长。
现在,我们要基于Student扩展出PrimaryStudent,可以先定义出PrimaryStudent:
function PrimaryStudent(props) {
// 调用Student构造函数,绑定this变量:
Student.call(this, props);
this.grade = props.grade || 1;
}
但是,调用了Student构造函数不等于继承了Student
PrimaryStudent创建的对象的原型是:
new PrimaryStudent() ----> PrimaryStudent.prototype ----> Object.prototype ----> null
必须想办法把原型链修改为:
new PrimaryStudent() ----> PrimaryStudent.prototype ----> Student.prototype ----> Object.prototype ----> null
如果按照简单粗暴的方法 即
PrimaryStudent.prototype = Student.prototype;
那定义这个PrimaryStudent没有意义了
必须借助一个中间对象来实现正确的原型链,
这个中间对象的原型要指向Student.prototype
中间对象可以用一个空函数F来实现
// PrimaryStudent构造函数:
function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || 1;
}
// 空函数F:
function F() {
}
// 把F的原型指向Student.prototype:
F.prototype = Student.prototype;
// 把PrimaryStudent的原型指向一个新的F对象,F对象的原型正好指向Student.prototype:
PrimaryStudent.prototype = new F();
// 把PrimaryStudent原型的构造函数修复为PrimaryStudent:
PrimaryStudent.prototype.constructor = PrimaryStudent;
// 继续在PrimaryStudent原型(就是new F()对象)上定义方法:
PrimaryStudent.prototype.getGrade = function () {
return this.grade;
};
// 创建xiaoming:
var xiaoming = new PrimaryStudent({
name: '小明',
grade: 2
});
xiaoming.name; // '小明'
xiaoming.grade; // 2
// 验证原型:
xiaoming.__proto__ === PrimaryStudent.prototype; // true
xiaoming.__proto__.__proto__ === Student.prototype; // true
// 验证继承关系:
xiaoming instanceof PrimaryStudent; // true
xiaoming instanceof Student; // true
函数F仅用于桥接,我们仅创建了一个new F()实例,而且,没有改变原有的Student定义的原型链
把继承这个动作用一个inherits()函数封装起来,还可以隐藏F的定义,并简化代码:
function inherits(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
inherits()函数可以复用:
function Student(props) {
this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
}
function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || 1;
}
// 实现原型继承链:
inherits(PrimaryStudent, Student);
// 绑定其他方法到PrimaryStudent原型:
PrimaryStudent.prototype.getGrade = function () {
return this.grade;
};
JavaScript的原型继承实现方式就是:
- 定义新的构造函数,并在内部用call()调用希望“继承”的构造函数,并绑定this;
- 借助中间函数F实现原型链继承,最好通过封装的inherits函数完成;
- 继续在新的构造函数的原型上定义新方法。继续在新的构造函数的原型上定义新方法。
作用域
- 作用域
○ 全局作用域
○ 局部作用域(函数作用域)
○ 词法作用域 → 作用域链 - 执行上下文
○ 变量环境
○ 作用域链
○ this 绑定
作用域链
只要是代码至少有一个作用域,写在函数内部的局部作用域,未写在任何函数内部即在全局作用域中;
如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域;
根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问,就称为作用域链。【就近原则】
作用域链本质上是底层的变量查找机制,在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域。
嵌套关系的作用域串联起来形成了作用域链
相同作用域链中按着从小到大的规则查找变量
子作用域能够访问父作用域,父级作用域无法访问子级作用域(就近原则)
function f1() {
var num = 123;
function f2() {
console.log( num );
}
f2();
}
var num = 456;
f1();
var a = 1;
function fn1() {
var a = 2;
var b = '22';
fn2();
function fn2() {
var a = 3;
fn3();
function fn3() {
var a = 4;
console.log(a); //a的值 ?
console.log(b); //b的值 ?
}
}
}
fn1();
闭包 (closure)
- 闭包指的是被包的函数,被包的函数有权访问其外一层作用域的变量。闭包本质仍是函数,就是返回一个子函数,子函数能访问父函数的局部变量
优点:● 数据私有化:避免变量污染,变量作用域延伸,提高代码安全性。● 维持状态:在多次执行时保持变量值,例如防抖、节流。
缺点:● 可能导致内存泄漏, 由于闭包引用外部变量,可能导致垃圾回收机制无法回收。(手动,在不需要时手动置 element = null)
应用:常见异步(定时器、事件监听、Ajax)、防抖、节流、Vue 响应式、React Hooks
function debounce(fn, delay) {
let timer = null; // timer 变量用于存储定时器 ID,实现私有化
return function () { // 返回一个闭包,保持对 timer 变量的访问
let context = this, args = arguments; // 保存 this 上下文和参数
if (timer) { // 如果已有定时器,则清除
clearTimeout(timer);
}
timer = setTimeout(() => { // 设置新的定时器
fn.apply(context, args); // 延迟执行目标函数
}, delay);
};
}
/*
● timer 变量在 debounce 内部,只能被返回的函数访问,避免了全局变量污染。
● 每次调用时 timer 保持最新状态,实现防抖效果。
*/
function throttle(fn, delay) {
let flag = true; // 标志位,初始为 true,表示可以执行
return function (...args) {
if (!flag) return; // 如果标志位为 false,直接跳出
flag = false; // 设置为 false,阻止接下来一段时间的执行
setTimeout(() => {
fn.apply(this, args); // 执行原函数
flag = true; // 恢复标志位,允许下一次执行
}, delay);
};
}
//vue响应式原理,通过闭包+依赖收集实现响应式
function reactive(obj) {
return new Proxy(obj, { // 创建 Proxy 代理对象
get(target, key) {
console.log(`读取 ${key}`);
return target[key]; // 读取对象属性
},
set(target, key, value) {
console.log(`更新 ${key} -> ${value}`);
target[key] = value; // 更新对象属性
return true;
}
});
}
let data = reactive({ count: 0 });
data.count++; // 读取 count -> 更新 count -> 1
//react hooks:useState 利用闭包存储状态。
function useState(initialValue) {
let state = initialValue; // 闭包变量 state
function setState(newValue) {
state = newValue; // 更新 state
console.log("State updated to:", state);
}
return [() => state, setState]; // 返回 getter 和 setter
}
const [getState, setState] = useState(0);
console.log(getState()); // 0
setState(5);
console.log(getState()); // 5
- 返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。因为 JavaScript 中的闭包会引用外部环境的变量,而这些变量的值在循环结束后可能已经发生了变化。
假设我们在循环中创建闭包,并将循环变量作为闭包函数的引用,最终所有闭包都会共享同一个变量的引用,并且这个变量在每次迭代时都会更新,从而导致闭包内部的值不是我们期望的。- 如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变。
//正确应用
for (var i = 0; i < lis.length; i++) {
// 利用for循环创建了4个立即执行函数
// 立即执行函数也成为小闭包因为立即执行函数里面的任何一个函数都可以使用它的i这变量
(function(i) {
lis[i].onclick = function() {
console.log(i);
}
})(i);
}
/*需求分析
打车起步价13(3公里内), 之后每多一公里增加 5块钱. 用户输入公里数就可以计算打车价格
如果有拥堵情况,总价格多收取10块钱拥堵费*/
var car = (function() {
var start = 13; // 起步价 局部变量
var total = 0; // 总价 局部变量
return {
// 正常的总价
price: function(n) {
if (n <= 3) {
total = start;
} else {
total = start + (n - 3) * 5
}
return total;
},
// 拥堵之后的费用
yd: function(flag) {
return flag ? total + 10 : total;
}
}
})();
console.log(car.price(5)); // 23
console.log(car.yd(true)); // 33
//错误实例
const funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i); // 打印 i 的值
});
}
// 由于 `i` 是 `var`,它在整个函数作用域内是共享的,循环结束时 i = 3
funcs[0](); // 输出 3
funcs[1](); // 输出 3
funcs[2](); // 输出 3
//解决方法
/*
使用 let 来替代 var(块级作用域)
let 会为每个循环迭代创建一个新的作用域,因此每次循环时,闭包捕获的 i 值是当前的值,不会和后续的值产生冲突。
*/
const funcs = [];
for (let i = 0; i < 3; i++) {
funcs.push(function() {
console.log(i); // 每个闭包捕获到的 `i` 是不同的
});
}
funcs[0](); // 输出 0
funcs[1](); // 输出 1
funcs[2](); // 输出 2
在没有class机制,只有函数的语言里,借助闭包,同样可以封装一个私有变量
闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来
工厂函数,因为它像工厂一样 生产对象。每次调用工厂函数时,都会返回一个新的对象,而这个对象可以包含不同的属性和方法。工厂函数通常用于实现对象的创建和封装,尤其是在没有 class 机制的情况下,工厂函数可以帮助你模仿类的行为。
//闭包也可以模拟class
function createCounter() {
let count = 0; // `count` 是私有变量
return {
increment: function() {
count++;
console.log(count);
},
decrement: function() {
count--;
console.log(count);
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
counter.increment(); // 输出 1
counter.increment(); // 输出 2
console.log(counter.getCount()); // 输出 2
counter.decrement(); // 输出 1
// 直接访问 `count` 会出错,因为它是私有的
console.log(counter.count); // undefined
- 闭包用于实现缓存
function memorize(fn) {
var cache = {}
return function() {
var args = Array.prototype.slice.call(arguments)//转换成array
var key = JSON.stringify(args)//将序列化的作为key
return cache[key] || (cache[key] = fn.apply(fn, args))
}
}
function add(a) {
return a + 1
}
var adder = memorize(add)
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(2) // 输出: 3 当前: cache: { '[1]': 2, '[2]': 3 }
//ES6版本
function memorize(fn) {
const cache = {}
return function(...args) {//换成了扩展运算符
const key = JSON.stringify(args)
return cache[key] || (cache[key] = fn.apply(fn, args))
}
}
function add(a) {
return a + 1
}
const adder = memorize(add)
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(1) // 输出: 2 当前: cache: { '[1]': 2 }
adder(2) // 输出: 3 当前: cache: { '[1]': 2, '[2]': 3 }
new专题
数字安全专题 (0.1+0.2!==0.3 的原因)
点击跳转
总结:JS数字采用 IEEE 754 标准 64 位双精度浮点数,1-11-52,用科学记数法表示指数和尾数,过长小数会被截断,导致精度误差。解决方法:高精度数值库Decimal.js;在ES6中,提供了Number.EPSILON属性,而它的值就是
2
−
52
2^{-52}
2−52
function numberepsilon(arg1,arg2){
return Math.abs(arg1 - arg2) < Number.EPSILON;
}
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
事件专题
- 事件循环
点击跳转 - 事件流:指在网页或应用程序中,事件发生时所遵循的特定顺序和路径。
捕获阶段->目标阶段->冒泡阶段
事件捕获/冒泡 (Event capture/bubbling) 从外到内 从内到外 默认冒泡
默认冒泡。在JavaScript中,可以使用 addEventListener 方法来指定在事件流的不同阶段对事件进行监听和处理。例如,通过设置 addEventListener 的第三个参数为 true ,可以在事件捕获阶段进行监听;设置为 false (默认值)则在事件冒泡阶段进行监听。
- 事件代理 Event Delegation
将事件处理程序添加到父元素上,来管理子元素的事件处理。具体来说,事件代理是指将事件绑定到父元素上,当子元素触发事件时,该事件会冒泡到父元素,由父元素来处理该事件。这样做的好处是可以减少事件处理程序的数量,减轻页面的负担,提高代码的性能和可维护性。
应用场景:(性能优化)减少内存消耗、事件动态添加 - mouseover v.s. mouseenter / mouseout v.s. mouseleave:是否冒泡、仅在元素触发/元素及子元素都触发
this的指向
普通函数没有明确调用者时 this 值为 window,严格模式下没有调用者时 this 的值为 undefined。
- 改变this指向的方法
- 共同点 : 都可以改变this指向
- 不同点:
- call 和 apply 会调用函数, 并且改变函数内部this指向.
- call 和 apply传递的参数不一样,call传递参数使用逗号隔开,apply使用数组传递
- bind 不会调用函数, 可以改变函数内部this指向.
- 应用场景
- call 经常做继承.
- apply经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值
- bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向.
- call()方法调用一个对象。简单理解为调用函数的方式,但是它可以改变函数的 this 指向。经常做继承.
var o = {
name: 'andy'
}
function fn(a, b) {
console.log(this);
console.log(a+b)
};
fn(1,2)// 此时的this指向的是window 运行结果为3
fn.call(o,1,2)//此时的this指向的是对象o,参数使用逗号隔开,运行结果为3
- apply() 方法调用一个函数。简单理解为调用函数的方式,但是它可以改变函数的 this 指向。应用场景: 经常跟数组有关系
var o = {
name: 'andy'
}
function fn(a, b) {
console.log(this);
console.log(a+b)
};
fn(1,2)// 此时的this指向的是window 运行结果为3
fn.apply(o,[1,2])//此时的this指向的是对象o,参数使用数组传递 运行结果为3
- bind() 方法不会调用函数,但是能改变函数内部this 指向,返回的是原函数改变this之后产生的新函数。如果只是想改变 this 指向,并且不想调用这个函数的时候,可以使用bind。应用场景:不调用函数,但是还想改变this指向
var o = {
name: 'andy'
};
function fn(a, b) {
console.log(this);
console.log(a + b);
};
var f = fn.bind(o, 1, 2); //此处的f是bind返回的新函数
f();//调用新函数 this指向的是对象o 参数使用逗号隔开
连续多个 bind,最后this指向是什么?
在 JavaScript 中,连续多次调用 bind 方法,最终函数的 this 上下文是由第一次调用 bind 方法的参数决定的。
const obj1 = { name: 'obj1' };
const obj2 = { name: 'obj2' };
const obj3 = { name: 'obj3' };
function getName() {
console.log(this.name);
}
const fn1 = getName.bind(obj1).bind(obj2).bind(obj3);
fn1(); // 输出 "obj1"
- 特殊的this指向
array.forEach(function(currentValue, index, arr), thisValue)
forEach方法有两个参数,第一个是回调函数,第二个是 this 指向的对象,第二个参数没有传入,默认为 undefined,所以会输出全局对象。
除了forEach方法,需要传入 this 指向的函数还有:every()、find()、findIndex()、map()、some(),在使用的时候需要注意。
var obj = {
arr: [1]
}
obj.arr.forEach(function() {
console.log(this)//window
})
- IIFE立即执行函数
立即执行函数就是定义后立刻调用的匿名函数。立即执行函数作为一个匿名函数,通常就是直接调用, this 是确定的,就是默认的全局对象 window。 - setTimeout 和 setInterval
setTimeout 和 setInterval 中函数的 this 指向规则是一样的。延时效果(setTimeout)和定时效果(setInterval)都是在全局作用域下实现的。无论是 setTimeout 还是 setInterval 里传入的函数,都会首先被交到全局对象手上。因此,函数中 this 的值,会被自动指向 window。
严格模式
JavaScript 除了提供正常模式外,还提供了严格模式(strict mode)。ES5 的严格模式是采用具有限制性 JavaScript变体的一种方式,即在严格的条件下运行 JS 代码。
严格模式在 IE10 以上版本的浏览器中才会被支持,旧版本浏览器中会被忽略。
严格模式对正常的 JavaScript 语义做了一些更改:
1.消除了 Javascript 语法的一些不合理、不严谨之处,减少了一些怪异行为。
2.消除代码运行的一些不安全之处,保证代码运行的安全。
3.提高编译器效率,增加运行速度。
4.禁用了在 ECMAScript 的未来版本中可能会定义的一些语法,为未来新版本的 Javascript 做好铺垫。比如一些保留字如:class,enum,export, extends, import, super 不能做变量名
- 开启严格模式
严格模式可以应用到整个脚本或个别函数中。因此在使用时,我们可以将严格模式分为脚本开启严格模式和为函数开启严格模式两种情况。
- 脚本开启严格模式
有的 script 脚本是严格模式,有的 script 脚本是正常模式,这样不利于文件合并,所以可以将整个脚本文件放在一个立即执行的匿名函数之中。这样独立创建一个作用域而不影响其他script 脚本文件。
(function (){
//在当前的这个自调用函数中有开启严格模式,当前函数之外还是普通模式
"use strict";
var num = 10;
function fn() {}
})();
//或者
<script>
"use strict"; //当前script标签开启了严格模式
</script>
<script>
//当前script标签未开启严格模式
</script>
- 函数开启严格模式
要给某个函数开启严格模式,需要把“use strict”; (或 ‘use strict’; ) 声明放在函数体所有语句之前。
function fn(){
"use strict";
return "123";
}
//当前fn函数开启了严格模式
- 严格模式产生的改变
'use strict'
num = 10
console.log(num)//严格模式后不能使用未声明的变量
------------------
var num2 = 1;
delete num2;//严格模式不允许删除变量
------------------
function fn() {
console.log(this); // 严格模式下全局作用域中函数中的 this 是 undefined
}
fn();
------------------
function Star() {
this.sex = '男';
}
// Star();严格模式下,如果构造函数不加new调用, this 指向的是undefined 如果给他赋值则会报错.
var ldh = new Star();
console.log(ldh.sex);
--------------------
setTimeout(function() {
console.log(this); //严格模式下,定时器 this 还是指向 window
}, 2000);
浏览器执行 JS原理
浏览器分成两部分:渲染引擎和 JS 引擎
渲染引擎:用来解析HTML与CSS,俗称内核,比如 chrome 浏览器的 blink ,老版本的 webkit
JS 引擎:也称为 JS 解释器。 用来读取网页中的JavaScript代码,对其处理后运行,比如 chrome 浏览器的 V8
浏览器本身并不会执行JS代码,而是通过内置 JavaScript 引擎(解释器) 来执行 JS 代码 。JS 引擎执行代码时逐行解释
每一句源码(转换为机器语言),然后由计算机去执行,所以 JavaScript 语言归为脚本语言,会逐行解释执行。
- js中的await 与 python中的await
await 是 JavaScript 中用于处理异步操作的一部分,它只能在 async 函数中使用,用来等待一个 Promise 完成并返回其结果。它的主要作用是让代码看起来像同步操作,但实际上仍然是异步执行。
简化了一堆.then()的链式调用
await 只会暂停当前的异步函数,而不会阻塞整个程序。
如果 await 后的 Promise 被拒绝(rejected),会抛出错误,需要用 try-catch 捕获。 -
- 扩展:python
python中逻辑同理,await 允许你等待这些操作完成,而不会阻塞事件循环,使得程序可以处理其他任务。异步操作通常涉及 I/O 密集型任务,比如:
网络请求(HTTP API 调用)
文件操作
数据库查询
计时器
- 扩展:python
- 页面加载时执行JavaScript函数
//js文件中写
//方法1
function myfun()
{ alert( "this window.onload" );
}
window.onload = myfun; /*用window.onload调用myfun()*/
//不要括号
//方法2
window.onload= function (){
func1();
func2();
func3();
}
概述
- 应用场景
表单动态校验(密码强度)
网页特效
服务端开发 Node.js
桌面程序 Electron
App
控制硬件-物联网 Ruff
游戏开发 cocos2d-js - javaScript(JS)包括:
- ECMAScript是JS的语言标准 如ES6
- DOM文档对象模型
- BOM浏览器对象模型,包含window,location,screen,history…
- window
alert()
confirm()
prompt()
setInterval()
setTimeout() - location
href
hash
url
reload() - history
go()
- window
引入
1、内部
放入<head>
<script type="text/javascript">js code</script>
2、外部:js文件引入 (更方便维护)
<script type="text/javascript" src="..."></script>
运行条件
要让浏览器运行JavaScript,必须先有一个HTML页面,在HTML页面中引入JavaScript,然后,让浏览器加载该HTML页面,就可以执行JavaScript代码。
注释
//
/* */
废弃的特性
很多类似这种的特性都被废除了,比如字符串对象,都是不再将被处理的对象作为参数传递,而是用实例其自带的方法调用。
❌ 非标准旧写法 | ✅ 推荐的标准写法 |
---|---|
Array.slice(myArr, 0, 2) | myArr.slice(0, 2) |
Array.forEach(myArr, fn) | myArr.forEach(fn) |
Array.map(myArr, fn) | myArr.map(fn) |
Array.filter(myArr, fn) | myArr.filter(fn) |
Array.join(myArr, ',') | myArr.join(',') |
读入数据
- json
fetch("data.json")
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("加载数据失败", error));
async function fetchData() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
const data = await response.json();
console.log(data);
} catch (error) {
console.error("请求失败", error);
}
}
fetchData();
//ES6 import
import userData from "./data.json" assert { type: "json" };
console.log(userData);
//main.js
import { user, getUser } from "./data.js";
console.log(user);
console.log(getUser());
//data.js
export const user = { name: "张三", age: 25 };
export function getUser() {
return user;
}
- 读取本地存储 (localStorage / sessionStorage)
// 存储数据
localStorage.setItem("user", JSON.stringify({ name: "张三", age: 25 }));
//JSON.stringify converts a JavaScript value to a JSON string
// 读取数据
const userData = JSON.parse(localStorage.getItem("user"));
console.log(userData);
- txt,csv
浏览器的 File 对象(即 event.target.files[0])不能直接访问内容,必须通过 FileReader 读取。
FileReader 是浏览器提供的 API,用于异步读取文件内容,它支持以下格式:
readAsText(file) —— 读取文本(如 .txt, .csv, .json)。
readAsDataURL(file) —— 读取图片并返回 Base64 编码。
readAsArrayBuffer(file) —— 读取二进制数据。
readAsBinaryString(file) —— 读取二进制字符串(已废弃)。
为什么必须写 onload?
readAsText(file) 是异步操作,读取文件需要时间,所以不能直接在下一行获取 result。必须等 onload 事件触发,文件数据才会被正确读取。
1️⃣ dragover 事件
当文件被拖动到 dropArea 上方时触发。
默认情况下,浏览器不允许放置文件,e.preventDefault(); 使其成为可拖拽区域。
2️⃣ drop 事件
当文件被放下时触发。
默认行为是打开文件,但我们希望自己控制读取文件,因此 e.preventDefault(); 阻止这个默认行为。
可以将读取到的文本转换成json
fetch("./data.txt") // 读取 txt 文件
.then(res => res.text()) // 读取文本内容
.then(text => {
try {
const jsonData = JSON.parse(text); // 将文本转换为 JSON
console.log(jsonData); // 输出 JSON 数据
} catch (error) {
console.error("JSON 解析失败:", error);
}
})
.catch(err => console.error("读取文件失败:", err));
<!-- 1 -->
不适用于:直接打开 HTML 文件时(因浏览器安全限制)。
fetch("example.txt")
.then(response => response.text())
.then(data => console.log(data))
.catch(error => console.error("读取文件失败", error));
<!-- 2 -->
<input type="file" id="fileInput">
<script>
document.getElementById("fileInput").addEventListener("change", function(event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = function(e) {
console.log(e.target.result); // 读取的文件内容
};
reader.readAsText(file);
});
</script>
<!-- 3 -->
<div id="dropArea" style="width: 300px; height: 150px; border: 2px dashed gray; text-align: center; line-height: 150px;">
拖拽 TXT 文件到此处
</div>
<pre id="fileContent"></pre>
pre是一个 HTML 标签,常用于显示文本内容,尤其是保留换行和空格格式的文本。
<script>
const dropArea = document.getElementById("dropArea");
dropArea.addEventListener("dragover", function(e) {
e.preventDefault(); // 允许拖拽
});
dropArea.addEventListener("drop", function(e) {
e.preventDefault();
const file = e.dataTransfer.files[0]; // 获取拖拽的文件
if (!file || file.type !== "text/plain") {
alert("请选择一个 TXT 文件!");
return;
}
const reader = new FileReader();
reader.onload = function(event) {
document.getElementById("fileContent").textContent = event.target.result;
};
reader.readAsText(file);
});
</script>
- 读取数据库
fetch("https://example.com/api/users")
.then(res => res.json())
.then(data => console.log(data));
变量
JavaScript严格区分大小写
JavaScript 是一种弱类型或者说动态语言。这意味着不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。
- 变量类型
-
- 基本数据类型
Number String Boolean Undefined Null
- 基本数据类型
-
- 引用数据类型
Object Array Function
- 引用数据类型
-
- 或者另外一种分类,数据类型分为两类:
简单类型(基本数据类型、值类型):在存储时变量中存储的是值本身,包括string ,number,boolean,undefined,null
复杂数据类型(引用类型):在存储时变量中存储的仅仅是地址(引用),通过 new 关键字创建的对象(系统对象、自定义对象),如 Object、Array、Date等;
- 或者另外一种分类,数据类型分为两类:
- NAN
NaN(Not a Number,非数)是计算机科学中数值数据类型的一类值,表示未定义或不可表示的值。常在浮点数运算中使用。Number("123nb")
会报错。它是一个全局属性,属于 Number 类型,数学运算错误\类型转换失败会得到。
NaN这个特殊的Number与所有其他值都不相等,包括它自己:
NaN === NaN; // false
//唯一能判断NaN的方法是通过isNaN()函数: true
Number.isNaN(NaN);
//isNaN()用来判断一个变量是否为非数字的类型,不是数字返回true,是数字返回false
bool_num=isNaN(num)
- null v.s. undefined
null表示一个“空”的值,它和0以及空字符串’‘不同,0是一个数值,’'表示长度为0的字符串,而null表示“空”。
undefined,它是只声明但未赋值的变量的默认值。
未声明过的值调用会报错,ReferenceError: myVar is not defined - 不声明的全局变量 以及 strict模式
如果一个变量没有通过var申明就被使用,那么该变量就自动被申明为全局变量。
ECMA在后续规范中推出了strict模式,在strict模式下运行的JavaScript代码,强制通过var申明变量,未使用var申明变量就使用的,将导致运行错误。
启用strict模式的方法是在JavaScript代码的第一行写上:‘use strict’;
这是一个字符串,不支持strict模式的浏览器会把它当做一个字符串语句执行,支持strict模式的浏览器将开启strict模式运行JavaScript。
var x=30;
var name="myy";
alert(typeof name);
console.log(name);
value=prompt(text,defaultText)//用于显示可提示用户进行输入的对话框。
document.write(x)//写在页面上
var htmlStr='<h1>'+x+'</h1>'
//undefine null NAN
var str;
console.log(str);
var variable = undefined;
console.log(variable + 'pink'); // undefinedpink
console.log(variable + 1); // NaN undefined 和数字相加 最后的结果是 NaN
// null 空值
var space = null;
console.log(space + 'pink'); // nullpink
console.log(space + 1); // 1
//几进制
num=0b1;
num=0o5;
num=0xa;
//显式变量转换
var a=Number("23");
var age = parseInt(prompt('请输入年龄:'));
var height = parseFloat(prompt('请输入身高(m):'));
var str=num.toString();//或String(num)
//支持隐式转换
var num=num+"string";//数字自动转换为字符串
num='12'-2;
内部函数可以访问外部函数定义的变量,反过来则不行
JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部
变量最后如果在全局作用域中也没有找到,则报ReferenceError错误
JavaScript默认有一个全局对象window,全局作用域的变量实际上被绑定到window的一个属性。
使用 let 声明的变量不会挂在全局对象 window 上,因此无法通过 window.variableName 的方式访问。
这与使用 var 声明的变量不同,var 声明的变量会被挂载在全局对象上,因此可以通过 window.variableName 的方式访问。
不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。
//把自己的代码全部放入唯一的名字空间MYAPP中,会大大减少全局变量冲突的可能
// 唯一的全局变量MYAPP:
var MYAPP = {};
// 其他变量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;
// 其他函数:
MYAPP.foo = function () {
return 'foo';
};
///
var foo = function () {}//定义的函数实际上也是一个全局变量
foo(); // 直接调用foo()
window.foo(); // 通过window.foo()调用。 效果一样。
//我们每次直接调用的alert()函数其实也是window的一个变量。
window.alert('调用window.alert()');
// 把alert保存到另一个变量:
var old_alert = window.alert;
// 给alert赋一个新函数:
window.alert = function () {}
alert(Number.MAX_VALUE); // 1.7976931348623157e+308
alert(Number.MIN_VALUE); // 5e-324
Infinity ,代表无穷大,大于任何数值
-Infinity ,代表无穷小,小于任何数值
NaN ,Not a number,代表一个非数值
- var v.s. let v.s. const
不声明直接赋值的也属于全局变量
为 window 对象动态添加的属性默认也是全局的,不推荐!
尽可能少的声明全局变量,防止全局变量被污染
全局变量:在任何一个地方都可以使用,只有在浏览器关闭时才会被销毁,因此比较占内存
局部变量:只在函数内部使用,当其所在的代码块被执行时,会被初始化;当代码块运行结束后,就会被销毁,因此更节省内存空间
- 作用域问题(块级作用域)
var 声明的变量具有函数作用域(function scope),而 let 具有块级作用(block scope)。const也会产生块作用域,只是不能修改值。
if (true) { var a = 10; }
console.log(a); // 10(`var` 变量泄露到 if 语句块外)
if (true) { let b = 20; }
console.log(b);
// ReferenceError: b is not defined (`let` 仅在 if 语句块内可用)
- 变量提升(Hoisting)但不初始化
var 变量会提升(hoist)到作用域顶部,并自动初始化为 undefined,但 let 也会提升但不会初始化,在访问前使用会报错。
console.log(x); // undefined
var x = 5;
console.log(y);
// ReferenceError: Cannot access 'y' before initialization
let y = 10;
- 避免全局污染
当 var 在全局作用域中声明时,它会成为 window 对象的属性,而 let 不会。
var globalVar = "hello";
console.log(window.globalVar); // "hello"
let globalLet = "hello";
console.log(window.globalLet); // undefined
- 避免重复声明
var 允许在同一作用域内多次声明同一个变量,而 let 会报错。
var a = 1;
var a = 2; // 允许
let b = 1;
let b = 2;
// SyntaxError: Identifier 'b' has already been declared
堆、栈
- 堆栈空间分配区别:
1、栈(操作系统):由操作系统自动分配释放存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈;简单数据类型存放到栈里面
2、堆(操作系统):存储复杂类型(对象),一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。 - 简单数据类型的存储方式
值类型变量的数据直接存放在变量(栈空间)中
- 复杂数据类型的存储方式
引用类型变量(栈空间)里存放的是地址,真正的对象实例存放在堆空间中
- 简单类型传参
函数的形参也可以看做是一个变量,当我们把一个值类型变量作为参数传给函数的形参时,其实是把变量在栈空间里的值复制了一份给形参,那么在方法内部对形参做任何修改,都不会影响到的外部变量。
function fn(a) {
a++;
console.log(a);
}
var x = 10;
fn(x);
console.log(x);
// 运行结果如下:
11
10
- 复杂数据类型传参
函数的形参也可以看做是一个变量,当我们把引用类型变量传给形参时,其实是把变量在栈空间里保存的堆地址复制给了形参,形参和实参其实保存的是同一个堆地址,所以操作的是同一个对象。
function Person(name) {
this.name = name;
}
function f1(x) { // x = p
console.log(x.name); // 2. 这个输出什么 ?
x.name = "张学友";
console.log(x.name); // 3. 这个输出什么 ?
}
var p = new Person("刘德华");
console.log(p.name); // 1. 这个输出什么 ?
f1(p);
console.log(p.name); // 4. 这个输出什么 ?
预解析
没声明时访问->报错,声明未赋值访问->undefined,let声明的变量不存在变量提示,实际开发中推荐先声明再访问变量
在 JavaScript 中,let 声明的变量在未定义之前不会被 JavaScript 解释器“提前(Hoisting)”到作用域的顶部,因此如果在声明之前访问它,会遇到 “ReferenceError: Cannot access before initialization” 错误。这是因为 let 变量存在 暂时性死区(Temporal Dead Zone, TDZ)。
变量提升(Hoisting) 只发生在 相同作用域 内,即同一个函数、代码块或全局作用域内。变量或函数声明会在 作用域的顶部 进行 预解析,但 var 和 let/const 在提升时的行为不同。
JavaScript 代码是由浏览器中的 JavaScript 解析器来执行的。JavaScript 解析器在运行 JavaScript 代码的时候分为两步:预解析和代码执行。
- 预解析:在当前作用域下, JS 代码执行之前,浏览器会默认把带有 var 和 function 声明的变量在内存中进行提前声明或者定义。预解析也叫做变量、函数提升。
- 代码执行: 从上到下执行JS语句。
预解析会把变量和函数的声明在代码执行之前执行完成。 - 变量预解析
变量提升(变量预解析): 变量的声明会被提升到当前作用域的最上面,变量的赋值不会提升。
console.log(num); // 结果是多少?
var num = 10;
//结果:undefined,变量提升只提升声明,不提升赋值
- 函数预解析
函数提升: 函数的声明会被提升到当前作用域的最上面,但是不会调用函数。函数表达式不存在提升的现象
fn();
function fn() {
console.log('打印');
}
结果:控制台打印字符串 — ”打印“
注意:函数声明代表函数整体,所以函数提升后,函数名代表整个函数,但是函数并没有被调用!
- 函数表达式声明函数问题
函数表达式创建函数,会执行变量提升,此时接收函数的变量名无法正确的调用:
fn();
var fn = function() {
console.log('想不到吧');
}
结果:报错提示 ”fn is not a function"
解释:该段代码执行之前,会做变量声明提升,fn在提升之后的值是undefined;而fn调用是在fn被赋值为函数体之前,此时fn的值是undefined,所以无法正确调用
条件判断
if(xx){
...
}else if(xx){
...
}else{
...
}
//三元运算符
condition?ok_then:no_then
JavaScript把null、undefined、0、NaN和空字符串’'视为false
switch(){
case label1:
xxx
break;
case label2:
xxx
break;
default:
xxx
break;
}
运算符
- 算数运算符
次方**
- 逻辑运算符
常用&& || !
- 三目运算符
三元运算符
条件?真run:假run
- 比较运算符
===
对应!==
,
===不会自动转换数据类型,如果数据类型不一致,返回false,如果一致,再比较。
undefined 和 null 与其自身严格相等
let res1 = (null === null) //true
let res2 = (undefined === undefined) //true
尽量不要使用= =比较,始终坚持使用= = =比较。
==
做类型转换,再进行值的比较
let res1 = (null == undefined ); // true
let res2 = (null === undefined); // false
!=
和!==
和!===
!=
是非严格不等于,允许类型转换。
!==
是严格不等于,不允许类型转换。
!===
是无效的运算符,不能使用。
- 赋值运算符
c+=5;
c++;
1、11 + “12”+ 13 的结果是什么? “111213”
2、true + false + 1 + “”的结果是什么?2
上述两个都会强行转换成数字类型。
console.log(parseInt("123.321test")); // 结果:123
console.log(parseFloat("123.321test")); // 结果:123.321
//parseInt 从字符串开头开始解析整数部分,遇到 .(小数点)后会停止解析。
//parseFloat 解析浮点数,包括小数部分,直到遇到非数值字符。
- 一元正号(Unary plus)将一个值强制转换为数字类型(Number):就是在值前面放
+
includes v.s. in
- includes() 可以在多种数据类型上使用,主要用于检查一个集合(或字符串)中是否包含某个特定的元素。
它在以下几种内置类型上都有定义:
Array.prototype.includes() (数组类型)
String.prototype.includes() (字符串类型)
TypedArray.prototype.includes() (类型化数组,例如 Int8Array, Uint8Array 等) - JavaScript 是有 in 运算符的用法的
in 运算符在 JavaScript 中是用来判断对象是否包含某个属性(无论是对象自身的属性还是继承来的属性)。- 与 hasOwnProperty() 的区别
in 运算符会检查对象自身及其原型链上是否存在某个属性。
而 Object.prototype.hasOwnProperty() 方法只检查对象自身是否拥有某个属性,不包括原型链上的属性。
- 与 hasOwnProperty() 的区别
循环
经典:for , while , do…while
for(let j=0;j<s.length;j++){}
while(xx){}
do { ... } while()//先执行一次 再判断
遍历集合/对象
- for…in 遍历的是key,用于数组会取到索引
for…in 循环在对数组进行迭代时,会返回 数组的索引,这些索引是以 字符串 的形式存在的,而不是 数字。这其实是因为 JavaScript 中的 数组 是一种特殊的对象,数组的索引实际上是字符串类型的 属性名,而不是数字类型的下标。
它会遍历原型链上的属性,这会导致一些“脏数据”被误遍历,尤其在老浏览器或使用 polyfill 时风险更大。能不用就不用,推荐用 Object.keys() / for…of / Object.entries()
- for…of遍历的是value,遍历对象时需要借助Object.values()/Object.keys()/Object.entries()
for…of 并不是专门用于对象的。它是用来遍历 可迭代对象(iterable objects)的,比如数组、字符串、Map、Set 等。
for (let key in obj) {
if (obj.hasOwnProperty(key)) { // 确保属性是对象自身的,而不是继承的
console.log(key, obj[key]);
}
}
//object类型本身不自带keys()/values()/entries()用法
//map,set自带
let keys = Object.keys(obj);//返回数组
let values = Object.values(obj);
let entries = Object.entries(obj);
entries.forEach(([key, value]) => {
console.log(key, value);
});
for (let [key, value] of Object.entries(obj)) {
console.log(key, value);
}
数组专用方法
- forEach执行副作用函数,它接收一个函数,每次迭代就自动回调该函数。
- map生成新数组,
数组的 map 和 forEach 方法默认不支持提前结束循环,即无法使用类似于 break 或 return 的语法来跳出循环。但是可以使用抛出异常的方式来达到提前结束循环的效果。
filter
reduce聚合数据
some 只要一个true就提前返回true
every 必须所有都是true才是true,只要一个false就提前返回false
find / findIndex :返回第一个通过测试的元素/元素索引
for (let index in arr) {
console.log(index); // 输出 "0", "1", "2",这些索引是字符串
}
const arr = [10, 20, 30];
for (let value of arr) {
console.log(value); // 输出 10, 20, 30
}
const arr = [1, 2, 3, 4, 5];
let isBreak = false;
try {
arr.forEach((item) => {
if (item === 3) {
throw new Error('break');
}
console.log(item);
});
} catch (e) {
if (e.message === 'break') {
console.log('循环提前结束');
} else {
throw e;
}
}
arr.forEach(function(value, index, array) {//没有返回值
//参数一是:当前数组元素值
//参数二是:当前数组元素的索引
//参数三是:当前的数组
//可省略某些参数
})
arr.forEach((value, index) => {
console.log(index, value); // 输出 "0 10", "1 20", "2 30"
});
array.map(callback)
/*
遍历数组的每一项
对每一项执行 callback 函数
返回一个新的数组(不改变原数组)
*/
//array.filter
var newArr = arr.filter(function(value, index,array) {
//参数一是:数组元素
//参数二是:数组元素的索引
//参数三是:当前的数组
return value >= 20;
});//返回新数组
//array.some查找数组中是否有满足条件的元素
var flag = arr.some(function(value,index,array) {
//参数一是:数组元素
//参数二是:数组元素的索引
//参数三是:当前的数组
return value < 3;
});//返回布尔值,只要查找到满足条件的一个元素就立马终止循环
//应用:搜索结果展示
search_pro.addEventListener('click', function() {
var arr = [];
data.some(function(value) {
if (product.value.includes(value.pname)) { // 修正错误的 in 用法
arr.push(value);
//return true; // 找到匹配项后提前终止循环
}
});
// 把拿到的数据渲染到页面中
setDate(arr);
});
map不是Map
一个是循环方法,一个是变量类型。
//Map的回调函数参数依次为value、key和map本身
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
let map = new Map();
map.set(key,value); //设置一个值
map.get(key) //获取一个值
map.delete(key) //删除一项
map.has(key) //判断有没有
map.clear() //清空
map.forEach((value, key) =>{
console.log(value, key);
})
array.map(callback)
/*
遍历数组的每一项
对每一项执行 callback 函数
返回一个新的数组(不改变原数组)
*/
解构赋值
直接对多个变量同时赋值。解构赋值还可以忽略某些元素。
var [x, y, z] = ['hello', 'JavaScript', 'ES6'];
/*如果数组本身还有嵌套,也可进行解构赋值,注意嵌套层次和位置要保持一致。*/
let [, , z] = ['hello', 'JavaScript', 'ES6']; // 忽略前两个元素,只对z赋值第三个元素
z; // 'ES6'
如果需要从一个对象中取出若干属性,也可以使用解构赋值,便于快速获取对象的指定属性
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school'
};
var {name, age, passport} = person;
对一个对象进行解构赋值时,同时可直接对嵌套的对象属性进行赋值,只要保证对应的层次是一致的。
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school',
address: {
city: 'Beijing',
street: 'No.1 Road',
zipcode: '100001'
}
};
var {name, address: {city, zip}} = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, 因为属性名是zipcode而不是zip
// 注意: address不是变量,而是为了让city和zip获得嵌套的address对象的属性:
address; // Uncaught ReferenceError: address is not defined
使用解构赋值对对象属性进行赋值时,如果对应的属性不存在,变量将被赋值为undefined,这和引用一个不存在的属性获得undefined是一致的。
如果要使用的变量名和属性名不一致,可以用下面的语法获取
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school'
};
// 把passport属性赋值给变量id:
let {name, passport:id} = person;
name; // '小明'
id; // 'G-12345678'
// 注意: passport不是变量,而是为了让变量id获得passport属性:
passport; // Uncaught ReferenceError: passport is not defined
解构赋值还可以使用默认值,这样就避免了不存在的属性返回undefined的问题
var person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678'
};
// 如果person对象没有single属性,默认赋值为true:
var {name, single=true} = person;
name; // '小明'
single; // true
有些时候,如果变量已经被声明了,再次赋值的时候,正确的写法也会报语法错误
// 声明变量:
var x, y;
// 解构赋值:
{x, y} = { name: '小明', x: 100, y: 200};
// 语法错误: Uncaught SyntaxError: Unexpected token =
这是因为JavaScript引擎把{开头的语句当作了块处理,于是=不再合法。解决方法是用小括号括起来:
({x, y} = { name: ‘小明’, x: 100, y: 200});
函数
函数作用域:全局作用域、局部作用域;
- 总结
- 函数内部声明的变量,在函数外部无法被访问
- 函数的参数也是函数内部的局部变量
- 不同函数内部声明的变量无法互相访问
- 函数执行完毕后,函数内部的变量实际被清空了
function func(parameter){
return parameter;
//return 只能返回一个值,如果有多个则返回最后一个
//无返回时是undefined
}
var func=function(a,b){return a+b;}
var r=func(2,3);
// 函数形参实参个数匹配
function getSum(num1, num2) {
console.log(num1 + num2);
// 1. 如果实参的个数和形参的个数一致 则正常输出结果
getSum(1, 2);
// 2. 如果实参的个数多于形参的个数 会取到形参的个数
getSum(1, 2, 3);
// 3. 如果实参的个数小于形参的个数 多于的形参定义为undefined 最终的结果就是 NaN
// 形参可以看做是不用声明的变量 num2 是一个变量但是没有接受值 结果就是undefined
getSum(1); // NaN
// 建议 我们尽量让实参的个数和形参相匹配
- 全局污染
引入的js文件中存在名字相同的情况,只会选择其一,后覆盖前。
window.a=a; //全局挂载
Function.call 的用法:强行把某个方法“借”到另一个值上执行(兼容性写法)。
IIFE(immediately invoked function expression)立即执行函数:创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量。是一种自执行匿名函数,这个匿名函数拥有独立的作用域。这不仅可以避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
类型转换
var str='121yyy32.9079478';
console.log(parseInt(str)); //12132
console.log(parseFloat(str));//type:number
console.log(Number(str));//NaN
console.log(isNaN(str));
console.log(str.toString());
String(str)
//隐式转换
console.log(""+num);//=="".concat(num);
//保留小数位数
console.log(num.toFixed(2));//返回字符串
var uri="http://.....html?name=zhangsan";
//uri编码
console.log(encodeURI(uri))
console.log(encodeURIComponent(uri))//完全解析 ? =都解析出来
//uri解码
decodeURI()
decodeURIComponent()
- Object.is(): 用来比较两个值是否相等
Object.is(+0, -0); //false
Object.is(NaN, NaN); false
- Object.assign():
用途: 1. 复制一个对象 2. 合并参数
let 新的对象 = Object.assign(目标对象, source1, srouce2....)
原则:后覆盖前
iterable
Array、Map和Set都属于iterable类型
具有iterable类型的集合可以通过新的for … of,for … in循环,它遍历的实际上是对象的属性名称。一个Array数组实际上也是一个对象,它的每个元素的索引被视为一个属性。
for...in... 与 for...of...的区别:
for ... in循环 将把name包括在内,但Array的length属性却不包括在内
for ... of循环 则完全修复了这些问题,它只循环集合本身的元素
var a = ['A', 'B', 'C'];
a.name = 'Hello';
for (var x of a) {
console.log(x); // 'A', 'B', 'C'
}
//Map:一组键值对,极快的查找速度
//一个key只能对应一个value,所以多次对一个key放入value,后面的值会把前面的值冲掉
var m = new Map(); // 空Map
var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95
m.get('Adam'); // undefined
//Map不能用索引取值,因为是按插入顺序保存键值对
m.set('Adam', 67); // 添加新的key-value
m.set('Bob', 59);
m.has('Adam'); // 是否存在key 'Adam': true
m.delete('Adam'); // 删除key 'Adam'
//set集合
//Set也是一组key的集合,但不存储value。
//由于key不能重复,所以在Set中,没有重复的key,重复元素在Set中自动被过滤
var s1 = new Set(); // 空Set
var s2 = new Set([1, 2, 3]); // 含1, 2, 3
s.add(key);
s.delete(key);
s.has(num) //判断是否存在num
异常
throw语句用来抛出一个用户自定义的异常。当前函数的执行将被停止(throw之后的语句将不会执行),并且控制将被传递到调用堆栈中的第一个catch块。如果调用者函数中没有catch块,程序将会终止。
function getRectArea(width, height) {
if (isNaN(width) || isNaN(height)) {
throw "Parameter is not a number!";
}
}
try {
getRectArea(3, 'A');
}
catch(e) {
console.log(e);
// expected output: "Parameter is not a number!"
}
try {
tryCode - 尝试执行代码块
}
catch(err) {
catchCode - 捕获错误的代码块
}
finally {
finallyCode - 无论 try / catch 结果如何都会执行的代码块
}
内置对象
- 查找文档:学习一个内置对象的使用,只要学会其常用成员的使用即可,我们可以通过查文档学习,可以通过MDN/W3C来查询。
Mozilla 开发者网络(MDN)提供了有关开放网络技术(Open Web)的信息,包括 HTML、CSS 和万维网及 HTML5 应用的 API。
MDN:https://developer.mozilla.org/zh-CN/ - 包装对象(Wrapper Objects)
- JavaScript 不会把所有基本数据类型“永久性地”或“始终”包装为复杂数据类型。它只会在你需要对基本数据类型进行“对象式操作”(即调用其方法或访问其属性)时,临时地将它们包装成对应的对象,操作完成后立即销毁这个临时对象。
- JavaScript 之所以被称为“弱类型”或“动态类型”语言,且表现出一定的灵活性,很大程度上得益于这种包装对象的机制。
当且仅当你尝试对一个基本数据类型的值进行**“对象式操作”**时,JavaScript 引擎会在后台自动进行以下步骤:
创建临时包装对象: 根据基本数据类型的类型,创建一个对应的“包装对象”实例。
对于 string,创建 String 对象。
对于 number,创建 Number 对象。
对于 boolean,创建 Boolean 对象。
执行操作: 在这个临时包装对象上执行你调用的方法或访问的属性。
销毁临时对象: 操作完成后,这个临时的包装对象会被立即销毁。
// 1. 生成临时变量,把简单类型包装为复杂数据类型
var temp = new String('andy');
//2. 调用相关方法
let upperStr = temp.toUpperCase();
// 3. 销毁临时变量
temp = null;
对象object
- JavaScript 一切皆对象,有他们各自的构造函数、方法属性,实例化。
- 包装类型:之所以具有对象特征的原因是字符串、数值、布尔类型数据是 JavaScript 底层使用 Object 构造函数“包装”来的,被称为包装类型。
无论是引用类型或是包装类型都包含两个公共的方法toString
和valueOf
,valueOf
方法获取原始值this.value,数据内部运算的基础,很少主动调用该方法 - 空间存储
普通对象在赋值时只是赋值栈内存中的地址,不是堆内存中的数据。 - 堆与栈的区别:
1.堆和栈是内存中的数据存储空间
2.简单类型的数据保存在内存的栈空间中
3.引用类型的数据保存在内存的堆空间中,栈内存中存取的是引用类型的地址
- 包装类型:之所以具有对象特征的原因是字符串、数值、布尔类型数据是 JavaScript 底层使用 Object 构造函数“包装”来的,被称为包装类型。
- 只要是类似集合的数据类型,都可以用for、map 、foreach、for…in来遍历。
- JavaScript的对象是一组由键-值组成的无序集合,键都是字符串类型,值可以是任意数据类型。
- 在 JavaScript 中,原型(prototype)是一个对象,它是另一个对象的继承来源。每个 JavaScript 对象都有一个内部属性 [[Prototype]](通常可以通过
__proto__
或Object.getPrototypeOf(obj)
来访问)。这个属性指向了该对象的原型。Object.prototype 是所有对象的原型链的终点,也就是说,所有对象最终都会继承自 Object.prototype,并且可以访问它上面定义的属性和方法。Object.prototype 是所有对象的原型,几乎所有的 JavaScript 对象都可以访问到 Object.prototype 上的方法。例如,toString()、hasOwnProperty()、valueOf() 等方法都定义在 Object.prototype 上。 - 原型链(Prototype Chain)
JavaScript 对象的原型链是由多个对象组成的链式结构。当你访问一个对象的属性时,JavaScript 会首先查找该对象自身是否具有这个属性。如果没有找到,它会继续查找该对象的原型,直到查找到 Object.prototype 为止。如果在 Object.prototype 也没有找到,那么会返回 undefined。
var person = {//注意只能用var申明一次
name: 'Bob',
age: 20,
tags: ['js', 'web', 'mobile'],
city: 'Beijing',
hasCar: true,
zipcode: null,
habit:function(){alert('habit') return'ok';}
};
// 利用构造函数创建对象
// 我们需要创建四大天王的对象 相同的属性: 名字 年龄 性别 相同的方法: 唱歌
// 构造函数的语法格式
// function 构造函数名() {
// this.属性 = 值;
// this.方法 = function() {}
// }
// new 构造函数名();
// 1. 构造函数名字首字母要大写
// 2. 我们构造函数不需要return 就可以返回结果
// 3. 我们调用构造函数 必须使用 new
// 4. 我们只要new Star() 调用函数就创建一个对象
// 5. 我们的属性和方法前面必须添加 this
function Star(uname, age, sex) {
this.name = uname;
this.age = age;
this.sex = sex;
this.sing = function(sang) {
console.log(sang);
}
}
var ldh = new Star('刘德华', 18, '男'); // 调用函数返回的是一个对象
// console.log(typeof ldh);
console.log(ldh.name);
console.log(ldh['sex']);
ldh.sing('冰雨');
var zxy = new Star('张学友', 19, '男');
console.log(zxy.name);
console.log(zxy.age);
zxy.sing('李香兰')
最后一个键值对不需要在末尾加逗号,如果加了,有的浏览器(如低版本的IE)将报错。
//object+类数组
delete object.p //删除对象属性或数组元素,不能删除变量、函数、常量,但不会更新length
//删除数组元素会留下一个“空洞”,即该位置的值为 undefined。
delete xiaoming.age; // 删除age属性
delete xiaoming.school; // 删除一个不存在的school属性也不会报错
//要检测xiaoming是否拥有某属性,可以用in操作符
'name' in xiaoming; // true
'grade' in xiaoming; // false
//如果in判断一个属性存在,这个属性不一定是xiaoming的,它可能是xiaoming继承得到的
//要判断一个属性是否是xiaoming自身拥有的,而不是继承得到的,可以用hasOwnProperty()方法
xiaoming.hasOwnProperty('name'); // true
xiaoming.hasOwnProperty('toString'); // false
xiaoming.isPrototypeOf(obj)//检查当前对象是否是另一个对象的原型。
Object.prototype//是所有对象都继承的原型对象,是所有对象的最顶层原型。
Object.prototype.name = value
//这种写法是将一个新的属性 name 添加到 所有对象 的原型链上,意味着所有的 JavaScript 对象都将能够访问到这个属性。
/*
所有通过字面量(如 let obj={})或者构造函数(如 new Object())创建的对象,
都会继承 Object.prototype 上的方法和属性。
*/
Object.keys(obj)//返回对象的所有属性名(键)组成的数组。
Object.values(obj)//返回对象的所有属性值组成的数组。
Object.entries(obj)//返回对象的所有属性键值对组成的二维数组。
//返回一个包含对象自身所有可枚举属性 [key, value] 对的二维数组。将对象转换为能通过数组迭代的格式,方便遍历。
Object.assign(target, source)//将 source对象的所有可枚举属性复制到 target 对象。
//复制一个对象、合并参数,原则:后覆盖前 是浅拷贝
let target = { a: 1, b: 2 };
let source1 = { b: 3, c: 4 };
let source2 = { d: 5 };
let 新的对象 = Object.assign({}, target, source1, source2);
console.log(新的对象); // { a: 1, b: 3, c: 4, d: 5 }
console.log(target); // { a: 1, b: 2 } -- 原目标对象没有被改变
Object.freeze(obj)//冻结对象,防止后续修改对象的属性。
object.push()
object.pop()
object.appendChild()
Object.is('a','a')//用来比较两个值是否相等
let {keys, values, entries} = Object;
for(let key of keys(json)){ //
console.log(key);
}
for(let value of values(json)){
console.log(value);
}
for(let item of entries(json)){
console.log(item);
}
for(let [key, val] of entries(json)){
console.log(key, val);
}
for (var k in obj) {
console.log(k); // k 变量 输出 得到的是 属性名
console.log(obj[k]); // obj[k] 得到是 属性值
}
//ES6特性
let json ={//简化
name, //name:name,
age , //age:age
/* showA:function(){
return this.name;
} */
showA(){//不要用箭头函数
return this.name;
},
};
// 深拷贝:所有层都可以拷贝下来
let obj = {
uname : '张三丰',
age : 22,
sex : '男',
color : ['red', 'blue', 'pink', 'yellow'],
meesage : {
index : 1,
score : 99
}
}
let newObj = {};
function kaobei (newObj, obj) {
// 遍历
for (let key in obj) {
if ( obj[key] instanceof Array ) { // obj[key] 是数组
// obj[key]是数组
newObj[key] = [];
kaobei(newObj[key], obj[key]);
} else if (obj[key] instanceof Object) {// obj[key] 是对象
// obj[key]再遍历拷贝
newObj[key] = {}
kaobei(newObj[key], obj[key]);
} else {
newObj[key] = obj[key];
}
}
console.log( obj, newObj );
}
kaobei(newObj, obj);
//获取更准确的类型判断。
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(typeof null) // "[object String]"
数学对象Math
属性、方法名 | 功能 |
---|---|
Math.PI | 圆周率 |
Math.floor() | 向下取整 |
Math.ceil() | 向上取整 |
Math.round() | 四舍五入版 就近取整 注意 -3.5 结果是 -3 |
Math.abs() | 绝对值 |
Math.max()/Math.min() | 求最大和最小值 |
Math.random() | 获取范围在[0,1)内的随机值 |
//获取指定范围内的随机整数
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Math.E //e
Math.LN10
Math.LN2
Math.LOG2E
Math.PI
Math.SQRT2 //根号2
Math.SQRT1_2 //0.5的平方根
Math.max()
Math.min()
Math.max.apply(null,arr);
Math.ceil(num)
Math.floor(num)
Math.round(num)
Math.random() //0<=random<1
random(0,36)//0~36随机
日期对象date
Date 对象和 Math 对象不一样,Date是一个构造函数,所以使用时需要实例化后才能使用其中具体方法和属性。
注意:如果创建实例时并未传入参数,则得到的日期对象是当前时间对应的日期对象
var now = new Date();//获取系统当前时间;
let date=new Date('1949-10-01')//指定时间
var oneday = new Date('December 25,1995 13:30:00');
var oneday2=new Date(1995,11,25);
var oneday3=new Date(1995,11,25,14,30,0);
var future = new Date('2019/5/1');
console.log(now.getDate());//获取月份的第几天1~31
console.log(now.getMonth());//获取月份0~11
console.log(now.getFullYear());//获取年份
console.log(now.getDay());//获取一星期中第几天0~6
console.log(now.getHours());//获取小时0~23
console.log(now.getMinutes());//获取分钟0~59
console.log(now.getSeconds());//获取秒数0~59
now.getMilliseconds(); // 毫秒数
Date.parse()//传入的字符串使用实际月份01~12,转换为Date对象后getMonth()获取的月份值为0~11
console.log(now.toDateString());//星期几 月 日 年
console.log(now.toTimeString());//时 分 秒 时区
//常用
now.toLocaleDateString()
now.toLocaleTimeString()
now.toLocaleString()
//时间戳 1970年头到现在的毫秒数
//获取总毫秒数 3种方法
var now = new Date();//获取系统当前时间;
now.getTime(); // [1] 1435146562875, 以number形式表示的时间戳
console.log(+new Date())//[2]
var now = Date.now();//[3] HTML5中提供的方法,有兼容性问题
//无需实例化 但是只能得到当前的时间戳, 而前面两种可以返回指定时间的时间戳
date.valueOf() //[4]
哈希表Map
特性 | Map | Object |
---|---|---|
键类型 | 任意类型(对象、函数、基本类型) | 只能是字符串或符号 |
键值顺序 | 有序(按插入顺序) | 无序 |
可迭代性 | 可直接使用 for...of 遍历 | 不能直接迭代 |
获取大小 | .size 属性 | 必须手动计算 |
操作类型 | 方法 / 语法 | 示例代码 | 说明 |
---|---|---|---|
创建 Map | new Map() | const map = new Map(); | 创建一个空的 Map |
创建 Map(带值) | new Map([[key1, val1], [key2, val2]]) | const map = new Map([['a', 1], ['b', 2]]); | 创建时直接初始化 |
添加键值对 | .set(key, value) | map.set('name', 'Alice'); | 添加或更新键值对 |
获取值 | .get(key) | map.get('name') | 获取对应键的值 |
检查键是否存在 | .has(key) | map.has('age') | 判断 Map 中是否有该键 |
删除键值对 | .delete(key) | map.delete('age') | 删除指定键的条目 |
清空 Map | .clear() | map.clear() | 删除所有键值对 |
获取 Map 大小 | .size | map.size | 获取键值对数量 |
遍历所有键值对 | for (const [k, v] of map) | for (const [k, v] of map) { console.log(k, v); } | 以插入顺序遍历键值对 |
遍历所有键 | map.keys() | for (const k of map.keys()) | 遍历所有键 |
遍历所有值 | map.values() | for (const v of map.values()) | 遍历所有值 |
forEach 遍历 | map.forEach((v, k) => {}) | map.forEach((v, k) => console.log(k, v)) | 使用回调函数遍历 |
使用对象做键 | map.set(obj, value) | map.set({id: 1}, 'data') | 可使用对象或函数作为键 |
Map 转数组 | Array.from(map) 或 [...map] | const arr = [...map] | 转换为数组 |
数组转 Map | new Map(array) | const map = new Map([['a', 1]]) | 使用二维数组创建 Map |
数组对象array
array属于对象object.JavaScript的数组类似于python中的list。也是可以嵌套的。可包括任意数据类型。
- 有些数组方法不会改变原数组,有些则会改变原数组。
改变原数组的方法:fill()、pop()、push()、shift()、splice()、unshift()、reverse()、sort(); - sort排序算法原理:插入排序、快排。
当数组长度小于等于10的时候,采用插入排序,大于10的时候,采用快排。 - splice() 和 slice()
- splice() 方法可以在数组中添加、删除或替换元素,并返回被删除的元素,它会改变原数组。
- slice() 方法是从原数组中返回指定开始和结束位置的元素组成的新数组,它不会改变原数组。
- 判断两个数组值是否相等
不能用等号判断,它是比较引用地址的
用JSON.stringify()判断,但缺点是如果是对象或嵌套数组的复杂数组,会报错 - 检测是否为数组
instanceof 运算符:instanceof 可以判断一个对象是否是某个构造函数的实例
var arr = [1, 23];
var obj = {};
console.log(arr instanceof Array); // true
console.log(obj instanceof Array); // false
Array.isArray()用于判断一个对象是否为数组,isArray() 是 HTML5 中提供的方法
var arr = [1, 23];
var obj = {};
console.log(Array.isArray(arr)); // true
console.log(Array.isArray(obj)); // false
- 不要对数组用 in 来判断值是否存在
数组用 includes(),不要用 in,因为它是判断索引是否存在
var a=[]
let counts=new Array(30).fill(0);//30个0
a.length
//直接给Array的length赋一个新的值会导致Array大小的变化(索引超过范围也可)。
arr.length = 6;
arr; // arr变为[1, 2, 3, undefined, undefined, undefined]
Array.isArray(list)
a.toString()//元素连成一串,逗号分隔
//join()把当前Array的每个元素都用指定的字符串连接起来并返回
//join默认分隔符拼接用','
//如果Array的元素不是字符串,将自动转换为字符串后再连接
var arr = ['A', 'B', 'C', 1, 2, 3];
arr.join('-'); // 'A-B-C-1-2-3'
//栈 push pop
a.push('a')//向Array的末尾添加若干元素
a.pop()//把Array的最后一个元素删除掉
//队列 shift unshift
a.unshift('a')//往Array的头部添加若干元素
a.shift('a')//把Array的第一个元素删掉.
a.reverse();//把整个Array的元素给反转
function compare(a,b){//反着写可以实现降序 等价于return b-a;
if(a<b)return -1;
else if (a>b)return 1;
else return 0;
//等价于 return a-b
}
a.sort(compare);//把所有元素先转换为String再排序,返回结果直接修改a
//字符串根据ASCII码进行排序,而小写字母a的ASCII码在大写字母之后.
a.indexOf('c')//搜索一个指定的元素的位置。没找到返回-1
//lastIndexOf
//当前的Array和另一个Array连接起来,并返回一个新的Array
//并没有修改当前Array,而是返回了一个新的Array
//可以接收任意个元素的Array,并且自动把Array包含的括号拆掉,然后全部添加到新的Array里
var arr = ['A', 'B', 'C'];
arr.concat(1, 2, [3, 4]); // ['A', 'B', 'C', 1, 2, 3, 4]
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let result = [...arr1, ...arr2]; // 使用扩展运算符追加数组
console.log(result); // 输出: [1, 2, 3, 4, 5, 6]
//slice()就是对应String的substring()版本 前闭后开
//它截取Array的部分元素,然后返回一个新的Array。
var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
arr.slice(0, 3); // 从索引0开始,到索引3结束,但不包括索引3: ['A', 'B', 'C']
arr.slice(3); // 从索引3开始到结束: ['D', 'E', 'F', 'G']
//splice万能方法:删除、插入、替换
/*
index 必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置(从1开始)。
howmany必需。要删除的项目数量。如果设置为 0,则不会删除项目。
item1, …, itemX —— 可选。向数组添加的新项目。
1.当不添加只删除时,没有可选参数,则(删除开始索引,删除个数)
2.当不删除只添加时,(添加开始索引,删除个数=0,items...)
3.删除且添加,(删除且添加的开始索引,删除个数,items...)
4.当删除个数等于添加个数时 实现替换功能
*/
var arr = ['Microsoft', 'Apple', 'Yahoo', 'AOL', 'Excite', 'Oracle'];
// 从索引2开始删除3个元素,然后再添加两个元素:
arr.splice(2, 3, 'Google', 'Facebook'); // 返回删除的元素 ['Yahoo', 'AOL', 'Excite']
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
// 只删除,不添加:
arr.splice(2, 2); // ['Google', 'Facebook']
arr; // ['Microsoft', 'Apple', 'Oracle']
// 只添加,不删除:
arr.splice(2, 0, 'Google', 'Facebook'); // 返回[],因为没有删除任何元素
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
//替换
arr.splice(1,1,'ok');
//查找 如果查不到返回-1
arr.indexOf('a',2)//从下标2开始会搜索指定字符串出现的位置:
arr.lastIndexOf('a',2)//从下标2开始从后往前查找出现的位置
//过滤 filter
var r = arr.filter(function (element, index, self) {...})
var result=arr.filter(function(item,index,array){
return item>10
});
var arr = ['A', '', 'B', null, undefined, 'C', ' '];
var r = arr.filter(function (s) {
return s && s.trim(); // 注意:IE9以下的版本没有trim()方法
//trim()去除字符串两边的空白
});
r; // ['A', 'B', 'C']
mapresult.forEach(function(item,index){
console.log(item);
})
//arr.map()在JavaScript的Array中,得到新的Array作为结果,把Array的所有数字转为字符串
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']
r=['1','2','3'].map(str => parseInt(str));
var mapresult=arr.map(function(item,index,array){
return item*2;
})
/*
arr.reduce(function(prev,cur,index,arr){
...
}, init);
arr :原数组;
prev :上一次调用回调时的返回值,或者初始值 init;
cur : 当前正在处理的数组元素;
index :当前正在处理的数组元素的索引,若提供 init 值,则索引为0,否则索引为1;
init :初始值
*/
//reduce()从左到右累加数据,直到最后为一个值(也就是最终的结果)
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
let min_str=strs.reduce((min,str)=>Math.min(min,str.length),Infinity);
//Infinity是初始值
console.log(min_str)
return s && s.trim()意思是
if (s) return s.trim();
else return s;
多维数组
如果数组的某个元素又是一个Array,则可以形成多维数组
var arr = [[1, 2, 3], [400, 500, 600], ‘-’];
0 | 1 | 2 |
---|---|---|
400 | 500 | 600 |
- | undefined | undefined |
//array
arrayObject.splice(index,howmany,item1,.....,itemX)
//数组中添加或删除项目,然后返回被删除的项目。该方法会改变原始数组。
/*
可删除从 index 处开始的零个或多个元素,并且用参数列表中声明的一个或多个值来替换那些被删除的元素。
参数 描述
index 必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。
howmany 必需。要删除的项目数量。如果设置为 0,则不会删除项目。
item1, ..., itemX 可选。向数组添加的新项目。
返回值 被删除的项目,如果没有删除元素,就返回一个空数组。
*/
Array.from: 把类数组(获取一组元素、arguments...) 对象转成数组,具备length就可以转换
//定义一个集合
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
//转成数组
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
//Array.from方法还可以接受第二个参数,
//作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组
//注意:如果是对象,那么属性需要写对应的索引
let arrayLike = {
"0": 1,
"1": 2,
"length": 2
}
let newAry = Array.from(arrayLike, item => item *2)//[2,4]
Array.of(): 把一组值,转成数组
let arr = Array.of('apple','banana','orange');
arr.sort();
arr.entries() //返回的是一个包含数组元素索引和对应值的迭代器(Iterator)。
//arr.map() 是数组专有的循环 非常有用,做数据交互映射,配合return,返回一个新数组
//没有return时==foreach
let newArr = arr.map((item, index, arr)=>{
let json={};
json.t = `^_^${item.title}-----`;//将title内容增加了符号
json.r = item.read+200;//变更了read人数
json.hot = item.hot == true && '真棒!!!';//将true显示为其他内容
return json;
});
//arr.filter() 过滤不合格 返回true 留下来
let newArr = arr.filter((item, index, arr)=>{
return item.hot==false;
});
//arr.some() 类似查找 数组里有某元素符合条件返回true
//arr.every() 数组里所有元素满足条件返回true
let b = arr.some((val, index, arr) =>{
return val=='banana2';
});
var b = arr.every((val, index, arr)=>{
return val%2==1;
});
arr.reduce() //用来对数组中的所有元素进行累积计算,最终得到一个单一的结果(值/数组/类数组)。
arr.reduceRight()
//比如:求数组的和、阶乘
const sum = numbers.reduce((accumulator, currentValue) => {
return accumulator + currentValue;
}, 0); // 初始值是 0
//找最大值
const max = numbers.reduce((accumulator, currentValue) => {
return accumulator > currentValue ? accumulator : currentValue;
});
/*
arr.reduce(callback(accumulator, currentValue[, index, array]), initialValue);
accumulator:保存结果的容器累积器,保存上次函数调用的返回值,或者是初始化的 initialValue。
currentValue:当前正在处理的数组元素。
index(可选):当前元素的索引。
array(可选):原数组。
initialValue(可选):累积器的初始值。
如果没有提供,则数组的第一个元素会被用作初始值,第二个元素会作为 currentValue 开始迭代。
如果数组为空,且没有提供 initialValue,reduce() 会抛出错误。如果提供了 initialValue,则直接返回它。
*/
arr.find()//查找,找出第一个符合条件的数组成员,如果没有找到,返回undefined
let res = arr.find((val, index, arr) =>{
return val>1000;
});
arr.findIndex()// 找的是位置, 没找到返回-1
arr.fill(填充的东西, 开始位置, 结束位置);
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
集合Set
没有索引index
字符串对象
- 字符串的不可变
指的是里面的值不可变,虽然看上去可以改变内容,但其实是地址变了,内存中新开辟了一个内存空间。
当重新给字符串变量赋值的时候,变量之前保存的字符串不会被修改,依然在内存中重新给字符串赋值,会重新在内存中开辟空间,这个特点就是字符串的不可变。
由于字符串的不可变,在大量拼接字符串的时候会有效率问题
字符串是不可变的,如果对字符串的某个索引赋值,不会有任何错误,但是也没有任何效果。
到底可变不可变?重新赋值整个字符串->可变,单独修改其中字符->不可变
JavaScript为字符串提供了一些常用方法,注意,调用这些方法本身不会改变原有字符串的内容,而是返回一个新字符串。
- 转义字符
\b 空格blank
//正则表达式
'a b c'.split(' '); // ['a', 'b', '', '', 'c']
//一个空格隔开的字符串
a b c'.split(/\s+/); // ['a', 'b', 'c'] //
//至少一个空格隔开的
'a,b, c d'.split(/[\s\,]+/); // ['a', 'b', 'c', 'd']
//至少一个空格或逗号隔开的
'a,b;; c d'.split(/[\s\,\;]+/); // ['a', 'b', 'c', 'd']
//至少一个空格或逗号或分号隔开的
var str='ok"fine"?'
var a='nihao'+521;//支持隐式转换
//如果'本身也是一个字符,那就可以用""括起来
//转义字符\
由于多行字符串用\n写起来比较费事,ES6标准新增了一种多行字符串的表示方法,用反引号表示
要把多个字符串连接起来,可以用+号连接:
//array+str
string.indexOf(char,start_pos) //返回在其中首次出现的位置 -1不存在 lastIndexOf
string.includes()//是否包含某个特定的元素,返回 true 或 false
//str
var name = '小明';
var age = 20;
var message = '你好, ' + name + ', 你今年' + age + '岁了!';
alert(message);
//ES6新增了一种模板字符串,它会自动替换字符串中的变量:
var name = '小明';
var age = 20;
var message = `你好, ${name}, 你今年${age}岁了!`;
alert(message);
parseFloat(str) 函数可解析一个字符串,并返回一个浮点数。
str.length
function countChar(str, char) {
return str.split(char).length - 1;//对字符串中的某个字符计数
}
str.charCodeAt(1)//获取指定位置1的字符对应编码ASCII
str.concat(str1,str2,..)//一般用+运算代替str.concat
str.trim()//去除字符串两端的空格(空白字符)并返回处理后的字符串。返回的是一个新的字符串。
//切片
str.slice(2)//从2到末尾截取==substring返回指定索引区间的子串
str.slice(2,4)//前闭后开
str.slice(-3)//跟python一样 从后往前数 返回-3~-1下标
var s = 'hello, world'
s.substring(0, 5); // 从索引0开始到5(不包括5),返回'hello'
s.substring(7); // 从索引7开始到结束,返回'world'
substr(start,cnt)//不同点:第二个参数是返回字符数
var s = 'Hello';
s.toUpperCase(); // 返回'HELLO'
var lower = s.toLowerCase(); // 返回'hello'并赋值给变量lower
lower; // 'hello'
toLocaleLowerCase()//根据所在地区 不同大小写的规则
toLocaleUpperCase()
indexOf(char,position)//从前往后搜索指定字符串出现的位置 如果有参数position 就找position及之后的
lastIndexOf()//从后往前查找出现的位置
var s = 'hello, world';
s.indexOf('world'); // 返回7
s.indexOf('World'); // 没有找到指定的子串,返回-1
str.startsWith()
str.endsWith()
str.repeat(count)
str.split(separator, limit);
//separator(可选):指定分隔符,可以是一个字符串或正则表达式。用于确定在哪里切分字符串。如果不传递该参数,整个字符串将作为一个单独的元素放入返回的数组。
//limit(可选):限制返回的数组的最大长度。如果指定了 limit,split() 将最多返回 limit 个元素。
str.replace(old,new)
substring和substr的区别
substring() 和 substr() 都是 JavaScript 字符串的方法,用于截取字符串的一部分。它们的区别在于参数的不同。
substring(起始位置,结束位置)
substr(起始位置,字符数)
Number
实例方法 toFixed
用于设置保留小数位的长度,注意返回字符串
Number 也可以当做普通函数使用,这时它的作用是强制转换成数值数据类型。
arguments
arguments 是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。参数不固定的时候用这个arguments。
- 类数组对象(Array-like Object)
arguments属于类数组对象(Array-like Object),虽然它像数组,但不能直接使用数组的方法(如 push 或 pop 、map、forEach等)。 - 常见的类数组对象
arguments 对象(在函数内部)
DOM 查询结果(如 document.querySelectorAll() 返回的 NodeList)
NodeList 和 HTMLCollection(例如 getElementsByTagName() 和 getElementsByClassName() 返回的集合) - arguments,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。这个存在的意义是访问函数的所有参数,特别是在函数的参数数量不确定时(例如,变参函数)。它包含了传递给函数的所有参数。
function foo(x) {
console.log('x = ' + x); // 10
for (var i=0; i<arguments.length; i++) {
console.log('arg ' + i + ' = ' + arguments[i]); // 10, 20, 30
}
}
foo(10, 20, 30);
利用arguments,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值
arguments.length 判断个数长度
return大坑
function foo() {
return
{ name: 'foo' };
}
foo(); // undefined
要小心了,由于JavaScript引擎在行末自动添加分号的机制,上面的代码实际上变成了
function foo() {
return; // 自动添加了分号,相当于return undefined;
{ name: 'foo' }; // 这行语句已经没法执行到了
}
所以正确写法为
function foo() {
return { // 这里不会自动加分号,因为{表示语句尚未结束
name: 'foo'
};
}
深浅拷贝
- 深拷贝
JSON.stringify()
JSON.parse(JSON.stringify(obj))是目前比较常用的深拷贝方法之一,它的原理就是利用JSON.stringify 将js对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象。
但是还存在问题,无法拷贝函数,undefined,symbol、不可枚举的属性、原型链,当使用过JSON.stringify()进行处理之后,都会消失;对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
- 函数库lodash的_.cloneDeep方法
- 手写
但同样是上面说过的问题没解决。
对象的属性里面成环,即循环引用没有解决。
递归的深度的深度太深就会引发栈内存的溢出
- 浅拷贝
直接赋值、Object.assign()、扩展运算符、手写
// 浅拷贝的实现;
function shallowCopy(object) {
// 只拷贝对象
if (!object || typeof object !== "object") return;
// 根据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {};
// 遍历 object,并且判断是 object 的属性才拷贝
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key];
//区别在这 不继续递归了
}
}
return newObject;
}
// 深拷贝的实现;
function deepCopy(object) {
if (!object || typeof object !== "object") return;
let newObject = Array.isArray(object) ? [] : {};
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] =
typeof object[key] === "object" ? deepCopy(object[key]) : object[key];
}
}
return newObject;
}
装饰器
JavaScript 的 装饰器(decorators) 是一种用于修改类和类方法行为的语法。装饰器本质上是一个函数,它可以用来增强、修改或替换类或类方法的功能。装饰器是 ES2022(即 ECMAScript 2022)的一部分,虽然目前很多浏览器还不完全支持,但你可以通过 Babel 等工具启用它。
装饰器的使用场景:
日志记录:自动记录方法调用的日志。
权限控制:在类方法前加上装饰器,进行权限验证。
缓存:给方法加上装饰器进行缓存,避免重复计算。
观察者模式:通过装饰器观察对象状态的变化。
需要注意的事项:
装饰器需要在 class 或方法定义前使用 @ 符号进行标记。
装饰器语法并不是 JavaScript 标准的一部分,虽然已经被提案并在 ECMAScript 2022 中正式接受,但在实际使用时仍需依赖工具(如 Babel)进行转译。
装饰器的行为修改是浅拷贝的,无法对类本身的实例进行深度操作。
JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数。
- 类装饰器
类装饰器用于修改类的构造函数或类本身。它是一个函数,接收类的构造函数作为参数,并返回一个修改后的构造函数。
function myDecorator(target) {
console.log(target); // 输出类的构造函数
target.prototype.decorated = true;
}
@myDecorator
class MyClass {}
const instance = new MyClass();
console.log(instance.decorated); // true
- 方法装饰器
方法装饰器用于修改类方法的行为。它接收三个参数:
类的原型(如果是实例方法)或构造函数(如果是静态方法)
方法名
方法描述符(descriptor)
/*
log 装饰器拦截了 add 方法的调用,打印了传入的参数和返回的结果。
方法装饰器通过 descriptor 对象修改方法的行为。
*/
function log(target, key, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${key} with arguments: ${args}`);
const result = originalMethod.apply(this, args);
console.log(`${key} returned: ${result}`);
return result;
};
return descriptor;
}
class MyClass {
@log
add(a, b) {
return a + b;
}
}
const instance = new MyClass();
instance.add(1, 2);
// 输出:
// Calling add with arguments: [1, 2]
// add returned: 3
var count = 0;
var oldParseInt = parseInt; // 保存原函数
window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); // 调用原函数
};
- 属性装饰器
属性装饰器用于修改类实例的属性。它只能作用于类的属性声明,但不能直接修改属性的值。
- 属性描述符(Property Descriptor)
属性描述符 是 JavaScript 中 用来描述对象属性特性 的对象。它定义了属性的 可写性、可枚举性 和 可配置性 等特性。
属性描述符分为两种类型:
- 数据属性(Data Descriptor)
- 访问器属性(Accessor Descriptor)
(1) 数据属性
数据属性是存储实际值的属性,包含以下四个特性:
特性 | 作用 |
---|---|
value | 该属性的值 |
writable | 是否可以修改该属性的值(true / false ) |
enumerable | 是否可以在 for...in 或 Object.keys() 里遍历(true / false ) |
configurable | 是否可以删除或修改该属性的特性(true / false ) |
(2) 访问器属性
访问器属性不存储值,而是通过 getter 和 setter 进行访问和修改。它包含以下四个特性:
get 获取属性时调用的函数
set 设置属性时调用的函数
enumerable 是否可以在 for…in 或 Object.keys() 里遍历
configurable 是否可以删除或修改该属性的特性
getter(获取器)和 setter(设置器)是对象的访问器属性,用于控制属性的获取和赋值行为,而不是直接存储值。
getter(获取器)用于 定义当访问属性时执行的函数。
通过 get 关键字定义。
getter 没有参数,必须返回一个值。
setter(设置器)用于 定义当设置属性时执行的函数。
通过 set 关键字定义。
setter 接受一个参数,表示新赋的值。
为什么要用getter ,setter?生成一个独立的属性
如果你只定义 getter 而不定义 setter,属性就是只读的
const person = {
firstName: "Alice",
lastName: "Smith",
get fullName() {
return this.firstName + " " + this.lastName;
},
set fullName(value) { // 定义 setter
const parts = value.split(" ");
this.firstName = parts[0];
this.lastName = parts[1];
}
};
person.fullName = "Bob Johnson"; // 触发 setter
console.log(person.firstName); // "Bob"
console.log(person.lastName); // "Johnson"
/*
readonly 装饰器使得 name 属性变为只读。
在尝试修改 name 时会抛出错误。
readonly 装饰器通过修改对象属性的 属性描述符(descriptor)来实现只读功能
属性描述符(Descriptor)
在 JavaScript 中,每个对象的属性都可以有一个 属性描述符(descriptor),这个描述符包含了该属性的一些特性,例如:
writable:决定属性是否可以被修改。
configurable:决定属性是否可以被删除或修改。
enumerable:决定属性是否可以出现在 for...in 循环和 Object.keys() 中。
get 和 set:用于定义 getter 和 setter 方法。
Object.defineProperty() 和 属性描述符
Object.defineProperty() 是 JavaScript 提供的一个方法,定义或修改对象的属性,同时也能设置这些属性的描述符。
*/
Object.defineProperty(obj, prop, descriptor)
Object.defineProperty(对象,修改或新增的属性名,{
value:修改或新增的属性的值,
writable:true/false,//如果值为false 不允许修改这个属性值
enumerable: false,//enumerable 如果值为false 则不允许遍历
configurable: false //configurable 如果为false 则不允许删除这个属性 属性是否可以被删除或是否可以再次修改特性
})
Object.defineProperty(obj, "name", {
value: "John",
writable: false, // 设置为只读
enumerable: true,
configurable: false
});
function readonly(target, key, descriptor) {
// 修改属性描述符
descriptor.writable = false; // 设置属性为只读
return descriptor; // 返回修改后的描述符
}
/*
target:指的是目标对象(类的原型,或者实例)。
key:指的是属性名或方法名。
descriptor:指的是属性或方法的描述符,其中包含了属性的各种设置(例如 writable、configurable、enumerable 等)。
*/
function readonly(target, key) {
const descriptor = {
writable: false
};
return descriptor;
}
class MyClass {
@readonly
name = "John Doe";
}
const instance = new MyClass();
console.log(instance.name); // "John Doe"
instance.name = "Jane Doe"; // TypeError: Cannot assign to read only property 'name'
- 访问器装饰器(getter/setter)
访问器装饰器可以应用于类的 getter 或 setter 方法,允许你对这些访问器进行修饰。
类的 getter 和 setter 方法是 JavaScript 中的一种特殊方法,用于访问和设置对象的属性。它们提供了对对象属性的封装,允许你在获取或修改属性时执行额外的逻辑。通过 getter 和 setter,你可以控制如何读取和修改对象的属性。
Getter(访问器)
getter 是一个方法,它允许你定义对象属性的读取行为。它会在访问该属性时自动调用。
Setter(设置器)
setter 是一个方法,它允许你定义对象属性的设置行为。它会在给该属性赋值时自动调用。
使用 getter 和 setter 的场景
数据验证:可以在设置属性值之前进行验证,确保其有效性。
懒加载:如果属性值比较复杂,可以在 getter 中做懒加载(延迟计算),避免每次访问都进行计算。
属性代理:你可以通过 getter 和 setter 来创建虚拟属性,这些属性并不直接存储在对象中,而是计算得出的。
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// Getter:懒加载 fullName
get fullName() {
console.log("Calculating full name...");
return `${this.firstName} ${this.lastName}`;
}
}
const user = new User("John", "Doe");
console.log(user.fullName); // "Calculating full name..." 和 "John Doe" 会输出
/*
有时,我们希望类的某些属性不能直接访问,可以通过添加前缀 _ 来表示它们是私有的,但实际还是通过 getter 和 setter 来访问它们。
*/
class MyClass {
constructor(value) {
this._value = value; // 私有属性,通常以 _ 开头
}
// Getter 方法
get value() {
console.log("Getting value");
return this._value;
}
// Setter 方法
set value(newValue) {
console.log("Setting value to " + newValue);
this._value = newValue;
}
}
function log(target, key, descriptor) {
const getter = descriptor.get;
descriptor.get = function() {
console.log(`Getting ${key}`);
return getter.apply(this);
};
return descriptor;
}
class MyClass {
constructor() {
this._age = 30;
}
@log
get age() {
return this._age;
}
}
const instance = new MyClass();
console.log(instance.age); // "Getting age" 然后 "30"
- 静态方法装饰器
静态方法装饰器与实例方法装饰器类似,但它应用于静态方法。
function staticDecorator(target, key, descriptor) {
console.log(`Static method ${key} is being decorated`);
}
class MyClass {
@staticDecorator
static staticMethod() {
console.log("Static method");
}
}
MyClass.staticMethod(); // Static method staticMethod is being decorated
高阶函数
高阶函数的定义:接受函数作为参数or把函数作为结果值返回。如:函数表达式,回调函数。
普通函数的声明与调用无顺序限制,推荐做法先声明再调用。** 函数表达式必须要先声明再调用**
//1 函数表达式:把函数当值赋值给变量
let counter=function(x,y){return x+y}
//2 回调函数:把函数作为参数传递给另外一个函数
function sum(arr) {
return arr.reduce(function (x, y) {//reduce降维
return x + y;
});
}
sum([1, 2, 3, 4, 5]); // 15
//3 函数作参数 回调函数
function fn(callback){
callback&&callback();
}
fn(function (){alert('h1')})
如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数.
当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数.调用函数f时,才真正计算求和的结果
//函数表达式:把函数当值赋值给变量
var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()
f(); // 15
我们在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”
注意一点,当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数
生成器generator
generator和函数不同的是,generator由function定义(注意多出的号),除了return语句,还可以用yield返回多次
function* fib(max) {
var t,
a = 0,
b = 1,
n = 0;
while (n < max) {
yield a;
[a, b] = [b, a + b];
n ++;
}
return;
}
fib(5);
//直接调用一个generator和调用函数不一样,fib(5)仅仅是创建了一个generator对象,还没有去执行它。
//调用generator对象有两个方法,一是不断地调用generator对象的next()方法:
var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}
/*
next()方法会执行generator的代码,
每次遇到yield x;就返回一个对象{value: x, done: true/false},然后“暂停”。
返回的value就是yield的返回值,
done表示这个generator是否已经执行结束了。
如果done为true,则value就是return的返回值。
当执行到done为true时,这个generator对象就已经全部执行完毕,不要再继续调用next()了。
*/
//第二个方法是直接用for ... of循环迭代generator对象,这种方式不需要我们自己判断done
for (var x of fib(10)) {
console.log(x); // 依次输出0, 1, 1, 2, 3, ...
}
try {
r1 = yield ajax('http://url-1', data1);
r2 = yield ajax('http://url-2', data2);
r3 = yield ajax('http://url-3', data3);
success(r3);
}
catch (err) {
handle(err);
}
generator还有另一个巨大的好处,就是把异步回调代码变成“同步”代码。这个好处类似于AJAX。
看上去是同步的代码,实际执行是异步的。
正则表达式(RegExp)
注意:如果是代码是被作为字符串插入到另一个代码的,要做好充分转义,放入new RegExp还要再加一层转义。
作用:表单验证、过滤敏感词、提取想要部分字符串
- ^作为取反符和边界符的区别
边界符:写在方括号外面
取反符:写在方括号里面
let rname=/expression/
// /表达式/就表示是正则表达式
regObj.test(被检测字符串)//检测是否与正则表达式匹配 返回true/false
regObj.exec(被检测字符串)//在一个指定字符串中执行一个搜索匹配
// 举例子:输出的结果["o", index: 4, input: "hello", groups: undefined]
//如果匹配成功,exec() 方法返回一个数组,否则返回null
/*全局匹配可以多次执行exec()方法来搜索一个匹配的字符串。
当我们指定g标志后,每次运行exec(),正则表达式本身会更新lastIndex属性,
表示上次匹配到的最后索引:*/
string.replace(/exp/,'new_word')
const match = string.match(regex)
/*
如果匹配成功,返回一个数组
如果没有匹配,返回 null
match[0] 整个正则表达式匹配到的字符串(整行)
match[1] 第一个 () 捕获组内容(标题前面的井号)
match[2] 第二个 () 捕获组内容(标题文本)
... 如果正则有更多 () 捕获组,就会继续往后放入
*/
//元字符 指表示特殊含义的符号,能够匹配一类 如[a-z]
//边界符:位置开头结尾
^开头
$结尾
//量词:重复次数
* //重复任意次 包括0次
+ //重复一次或更多次
? //重复0次或一次
{n}
{n,}
{n,m} //逗号两侧不要出现空格
//字符类
[匹配字符集合] 如[abc]
[a-z]
[a-zA-Z]
[0-9]
[a-zA-Z0-9-_]
[^abc] //取反符号 不能是abc
. //匹配除换行符之外任何单个字符
\d [0-9]
\D [^0-9]
\w [a-zA-Z0-9-_]
\W 除\w之外
\s 匹配空格包括换行符、制表符、空格符等 [\t\r\n\v\f]
\S 匹配除\s之外
| 用来表示“匹配其中任意一个”的意思。
//例子
^\d{4}-\d{1,2}-\d{1,2}\ //日期
/^[a-zA-Z0-9-_]{6,10}$/ //用户名
/^1(3\d|4[5-9]|5[0-35-9]|6[567]|7[0-8]|8\d|9[0-35-9])\d{8}$/ //手机号
//修饰符
/表达式/修饰符
i //ignore 不区分大小写
g //global 匹配所有满足正则表达式的结果
m//表示执行多行匹配
gi //组合使用
‘00\d’可以匹配’007’
\s可以匹配一个空格(也包括Tab等空白符),所以\s+表示至少有一个空格
由于’-‘是特殊字符,在正则表达式中,要用’'转义
[0-9a-zA-Z\_]可以匹配一个数字、字母或者下划线;
[0-9a-zA-Z\_]+可以匹配至少由一个数字、字母或者下划线组成的字符串
[a-zA-Z\_\$][0-9a-zA-Z\_\$]*可以匹配由字母或下划线、$开头,后接任意个由一个数字、字母或者下划线、$组成的字符串
[a-zA-Z\_\$][0-9a-zA-Z\_\$]{0, 19}更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)
A|B可以匹配A或B,所以(J|j)ava(S|s)cript可以匹配’JavaScript’、‘Javascript’、‘javaScript’或者’javascript’
- 分组
用()表示的就是要提取的分组
如果正则表达式中定义了组,就可以在RegExp对象上用exec()方法提取出子串来。
exec()方法在匹配成功后,会返回一个Array,第一个元素是正则表达式匹配到的整个字符串,后面的字符串表示匹配成功的子串
exec()方法在匹配失败时返回null - 贪婪匹配 or 非贪婪匹配
正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。
.*
v.s..*?
var re = /^(\d+)(0*)$/;
re.exec('102300'); // ['102300', '102300', '']
\d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了
必须让\d+采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,加个?就可以让\d+采用非贪婪匹配
var re = /^(\d+?)(0*)$/;
re.exec('102300'); // ['102300', '1023', '00']
- 实战
//表单验证
<input type="text" class="uname"> <span>请输入用户名</span>
<script>
// 量词是设定某个模式出现的次数
var reg = /^[a-zA-Z0-9_-]{6,16}$/; // 这个模式用户只能输入英文字母 数字 下划线 中划线
var uname = document.querySelector('.uname');
var span = document.querySelector('span');
uname.onblur = function() {
if (reg.test(this.value)) {
console.log('正确的');
span.className = 'right';
span.innerHTML = '用户名格式输入正确';
} else {
console.log('错误的');
span.className = 'wrong';
span.innerHTML = '用户名格式输入不正确';
}
}
</script>
//过滤敏感词汇
<textarea name="" id="message"></textarea> <button>提交</button>
<div></div>
<script>
var text = document.querySelector('textarea');
var btn = document.querySelector('button');
var div = document.querySelector('div');
btn.onclick = function() {
div.innerHTML = text.value.replace(/激情|gay/g, '**');
}
</script>
面向对象编程
- 面向过程v.s. 面向对象
面向过程的唯一优点是性能高,一般在单片机中使用。
面向对象的优点:封装、继承、多态性,易维护、易复用、易扩展,便于设计出低耦合的系统。 - 基本概念
对象的两个基本概念:
1、类:类是对象的类型模板,例如,定义Student类来表示学生,类本身是一种类型,Student表示学生类型,但不表示任何具体的某个学生;
2、实例:实例是根据类创建的对象,例如,根据Student类可以创建出xiaoming、xiaohong、xiaojun等多个实例,每个实例表示一个具体的学生,他们全都属于Student类型。
- 实例成员就是构造函数内部通过this添加的成员,实例成员只能通过实例化的对象来访问。静态成员是在构造函数本身上添加的成员,静态成员只能通过构造函数来访问。
function Star(uname, age) {
this.uname = uname;
this.age = age;
this.sing = function() {
console.log('我会唱歌');
}
}
var ldh = new Star('刘德华', 18);
console.log(ldh.uname);//实例成员只能通过实例化的对象来访问
Star.sex = '男';
var ldh = new Star('刘德华', 18);
console.log(Star.sex);//静态成员只能通过构造函数来访问
- 对象原型
原形:一个对象,我们也称为 prototype 为原型对象(是构造函数的一个属性,它的数据类型是对象)。作用是共享方法,结合构造函数原型的特征,实际开发重往往会将封装的功能函数添加到原型对象中。
构造函数的原型对象:每一个构造函数都有一个名为 prototype 的属性,译成中文是原型的意思,prototype 的是对象类据类型,每个原型对象都具有 constructor 属性代表了该原型对象对应的构造函数,JavaScript 中大多是借助原型对象实现继承的特性。
对象都会有一个属性__proto__
指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 __proto__
原型的存在。
__proto__
对象原型和原型对象 prototype 是等价的
__proto__
对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象 prototype
//原型修改之后一定要为prototype.constructor重新赋值
Chinese.prototype = people;
Chinese.prototype.constructor = Chinese;
- constructor构造函数
用new调用的函数就是构造函数,这行为叫实例化,没有参数时可以省略new x(这个括号),返回值为新创建的对象,构造函数内部的return返回值无效。
// 定义函数
function foo() {
console.log('通过 new 也能调用函数...');
}
// 调用函数
new foo;
构造函数首字母要大写,结合new使用。
new 在执行时会做四件事情:
① 在内存中创建一个新的空对象。
② 让 this 指向这个新的对象。
③ 执行构造函数里面的代码,给这个新对象添加属性和方法。
④ 返回这个新对象(所以构造函数里面不需要 return )。
对象原型( __proto__
)和构造函数(prototype)原型对象里面都有一个属性 constructor 属性 ,constructor 我们称为构造函数,因为它指回构造函数本身。
constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数如:
function Star(uname, age) {
this.uname = uname;
this.age = age;
}
// 很多情况下,我们需要手动的利用constructor 这个属性指回 原来的构造函数
Star.prototype = {
// 如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数
constructor: Star, // 手动设置指回原来的构造函数
sing: function() {
console.log('我会唱歌');
},
movie: function() {
console.log('我会演电影');
}
}
var zxy = new Star('张学友', 19);
console.log(zxy)
构造函数好用但存在浪费内存的问题。
- 构造函数原型prototype
构造函数通过原型分配的函数是所有对象所共享的。
JavaScript 规定,每一个构造函数都有一个prototype 属性,指向另一个对象。注意这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。
我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
function Star(uname, age) {
this.uname = uname;
this.age = age;
}
Star.prototype.sing = function() {
console.log('我会唱歌');
}
var ldh = new Star('刘德华', 18);
var zxy = new Star('张学友', 19);
ldh.sing();//我会唱歌
zxy.sing();//我会唱歌
- 实现
- 早期JavaScript通过基于构造函数和原型(prototype)来实现面向对象编程。
原型是指当我们想要创建xiaoming这个具体的学生时,我们并没有一个Student类型可用。那怎么办?恰好有这么一个现成的对象:
var Student = {//这就是一个对象
name: 'Robot',
height: 1.2,
run: function () {
console.log(this.name + ' is running...');
}
};
var xiaoming = {
name: '小明'
};
xiaoming.__proto__ = Student;
//构造函数创建对象
function Star(name, age) {
this.name = name;
this.age = age;
}
var ldh = new Star('刘德华', 18)//实例化对象
console.log(ldh);
xiaoming有自己的name属性,但并没有定义run()方法。不过,由于小明是从Student继承而来,只要Student有run()方法,xiaoming也可以调用
JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有对象都是实例,所谓继承关系不过是把一个对象的原型指向另一个对象而已。
在JavaScrip代码运行时期,你可以把xiaoming从Student变成Bird,或者变成任何对象。
在编写JavaScript代码时,不要直接用obj.__proto__
去改变一个对象的原型,并且,低版本的IE也无法使用__proto__
。Object.create()方法可以传入一个原型对象,并创建一个基于该原型的新对象,但是新对象什么属性都没有。
// 原型对象:
var Student = {
name: 'Robot',
height: 1.2,
run: function () {
console.log(this.name + ' is running...');
}
};
function createStudent(name) {
// 基于Student原型创建一个新对象:
var s = Object.create(Student);
// 初始化新对象:
s.name = name;
return s;
}
var xiaoming = createStudent('小明');
xiaoming.run(); // 小明 is running...
xiaoming.__proto__ === Student; // true
方法
在一个对象中绑定函数,称为这个对象的方法。
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var y = new Date().getFullYear();
return y - this.birth;
}
};
xiaoming.age; // function xiaoming.age()
xiaoming.age(); // 今年调用是25,明年调用就变成26了
//拆开写也可以
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25, 正常结果
getAge(); // NaN
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空
如果单独调用函数,比如getAge(),此时,该函数的this指向全局对象,也就是window。
strict模式下指向undefined
要指定函数的this指向哪个对象,可以用函数本身的apply方法,它接收两个参数,第一个参数就是需要绑定的this变量,第二个参数是Array,表示函数本身的参数。
对普通函数调用,我们通常把this绑定为null
另一个与apply()类似的方法是call(),唯一区别是:
apply()把参数打包成Array再传入;
call()把参数按顺序传入。
Math.max(3, 5, 4),分别用apply()和call()实现
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5
- ES6引入class
类名常规首字母大写。
创建类 类名后面不要加小括号
constructor 函数 只要 new 生成实例时,就会自动调用这个函数, 如果我们不写这个函数,类也会自动生成这个函数,构造函数不需要加function
多个函数方法之间不需要添加逗号分隔
//步骤1 使用class关键字
class name {
// class body
}
//步骤2使用定义的类创建实例 注意new关键字
var xx = new name();
//类中封装的并不是变量和函数,因此不能使用关键字let、const、var
// 1. 创建类 class 创建一个 明星类
class Star {
// 类的共有属性放到 constructor 里面,返回实例对象
constructor(name, age) {
this.name = name;
this.age = age;
}
//static 静态属性或方法
//注意,方法与方法之间不需要添加逗号
sing(song) {
console.log(this.uname + '唱' + song);
}
}
// 2. 利用类创建对象 new
var ldh = new Star('刘德华', 18);
console.log(ldh);
- 继承
- 继承中,如果实例化子类输出一个方法,先看子类有没有这个方法,如果有就先执行子类的.如果子类里面没有,就去查找父类有没有这个方法,如果有,就执行父类的这个方法(就近原则)
- 如果子类想要继承父类的方法,同时在自己内部扩展自己的方法,利用super 调用父类的构造函数,super 必须在子类this之前调用
- 时刻注意this的指向问题,类里面的共有的属性和方法一定要加this。constructor中的this指向的是new出来的实例对象 ,自定义的方法,一般也指向的new出来的实例对象,绑定事件之后this指向的就是触发事件的事件源
- 在 JavaScript 中,that 不是一个关键字或内置对象,但在 ES6 之前的代码中,开发者常使用 that 作为一个变量名,用来保存 this 的引用,特别是在 回调函数 或 闭包 中,以避免 this 绑定丢失的问题。
var that = this; // 保存 this 的引用
- 在 ES6 中类没有变量提升,所以必须先定义类,才能通过类实例化对象
- super关键字 用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数。
// 父类
class Father{
}
// 子类继承父类
class Son extends Father {
}
//子类使用super关键字访问父类的方法
//super 函数的作用是可以将子类实例化时获得的参数传入父类的构造函数之中。
//定义父类
class Father {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum() {
console.log(this.x + this.y);
}
}
//子元素继承父类
class Son extends Father {
constructor(x, y) {
super(x, y); //使用super调用了父类中的构造函数
}
}
var son = new Son(1, 2);
son.sum(); //结果为3
// 父类有加法方法
class Father {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum() {
console.log(this.x + this.y);
}
}
// 子类继承父类加法方法 同时 扩展减法方法
class Son extends Father {
constructor(x, y) {
// 利用super 调用父类的构造函数 super 必须在子类this之前调用,放到this之后会报错
super(x, y);
this.x = x;
this.y = y;
}
subtract() {
console.log(this.x - this.y);
}
}
var son = new Son(5, 3);
son.subtract(); //2
son.sum();//8
//super可以调用父类普通函数
class Father {
say() {
return '我是爸爸';
}
}
class Son extends Father { // 这样子类就继承了父类的属性和方法
say() {
// super.say() super 调用父类的方法
return super.say() + '的儿子';
}
}
var damao = new Son();
console.log(damao.say());
本地存储webstorage
webstorage是本地存储,存储在客户端,包括localStorage和sessionStorage
数据存储在用户浏览器中。
容量较大,sessionStorage和localStorage约 5M 左右
只能存储字符串,可以将对象JSON.stringify() 编码后存储
- localStorage
localStorage生命周期是永久,同一浏览器可以共享,这意味着除非用户显示在浏览器提供的UI上清除localStorage信息,否则这些信息将永远存在。存放数据大小为一般为5MB,而且它仅在客户端(即浏览器)中保存,不参与和服务器的通信,键值对形式。
localStorage.setItem("key","value");//以“key”为名称存储一个值“value”
localStorage.getItem("key");//获取名称为“key”的值
localStorage.removeItem("key");//删除名称为“key”的信息。
localStorage.clear();//清空localStorage中所有信息 清空所有的本地存储数据
localStorage.setItem('key',value)
value为一个对象Object,以上面的方式写入,会出现读取的返回值为{object Object}的情况,但这并不是我们想要的,此时我们需要使用新的方式传入Object
localStorage.setItem('param',JSON.stringify(Object))
JSON.parse(localStorage.getItem('key'))
- sessionStorage
1、生命周期为关闭浏览器窗口,仅在当前会话下有效,关闭页面或浏览器后被清除。
2、在同一个窗口(页面)下数据可以共享
- 以键值对的形式存储使用
- 用法跟localStorage 基本相同
存储-自定义属性
自定义属性: 由程序员自己添加的属性,在DOM对象中找不到, 无法使用点语法操作,必须使用专门的API
getAttribute('属性名') // 获取自定义属性
setAttribute('属性名', '属性值') // 设置自定义属性
removeAttribute('属性名') // 删除自定义属性
//另一种 data-自定义属性
//传统的自定义属性没有专门的定义规则,开发者随意定值,不够规范
//在html5中推出来了专门的data-自定义属性
//在标签上一律以data-开头
//在DOM对象上一律以dataset对象方式获取
<div class='box' data-id='10'></div>
console.log(box.dataset.id)
Cookie
- document对象还有一个cookie属性,可以获取当前页面的Cookie。
document.cookie
。为了解决安全隐患,服务器在设置Cookie时可以使用httpOnly,设定了httpOnly的Cookie将不能被JavaScript读取。这个行为由浏览器实现,主流浏览器均支持httpOnly选项 为了确保安全,服务器端在设置Cookie时,应该始终坚持使用httpOnly。 - Cookie是由服务器发送的key-value标示符。因为HTTP协议是无状态的,但是服务器要区分到底是哪个用户发过来的请求,就可以用Cookie来区分。
当一个用户成功登录后,服务器发送一个Cookie给浏览器,例如user=ABC123XYZ(加密的字符串)…,此后,浏览器访问该网站时,会在请求头附上这个Cookie,服务器根据Cookie即可区分出用户。
Cookie还可以存储网站的一些设置,例如,页面显示的语言等等。 - cookie 里面都包含什么属性
① Domain
Domain 属性告诉浏览器允许哪些主机访问 cookie。未指定,默认为设置 cookie 的同一主机。因此,当使用客户端 JavaScript 访问 cookie 时,只能访问与 URL 域相同的 cookie;只有与 HTTP 请求的域共享相同域的 cookie 可以与请求头一起发送到服务端。
② Path
指定访问 cookie 的请求 URL 中的路径。除了将 cookie 限制到域之外,还可以通过路径来限制它。 路径属性为 Path=/store 的 cookie 只能在路径 /store 及其子路径 /store/cart、/store/gadgets 等上访问。
③ Expires/Max-size
该属性用来设置 cookie 的过期时间。若设置其值为一个时间,那么当到达此时间后,cookie 就会失效。不设置的话默认值是 Session,意思是cookie会和session一起失效。当整个浏览器关闭后,cookie 就会失效。除此之外,它还可以通过将过期日期设置为过去来删除 cookie。
④ Secure
具有 Secure 属性的 cookie 仅可以通过安全的 HTTPS 协议发送到服务器,而不会通过 HTTP 协议。这有助于通过使 cookie 无法通过不安全的连接访问来防止中间人攻击。除非网站用不安全的 HTTP 连接,否则应该始终将此属性与所有 cookie 一起使用。
⑤ HTTPOnly
此属性使 cookie 只能通过服务端访问。 因此,只有服务端可以通过响应头设置它们,然后浏览器会将它们与每个后续请求的头一起发送到服务器,并且它们将无法通过客户端 JavaScript 访问。
在一定程度上帮助保护带有敏感信息(如身份验证 token)的 cookie 免受 XSS 攻击,因为任何客户端脚本都无法读取 cookie。但这并不完全免受 XSS 攻击。如果攻击者在网站上执行第三方脚本,那可能无法访问 cookie,但他们可以直接向服务端执行相关的 API 请求。因此,如果用户访问一个页面,黑客在网站上注入了恶意脚本,则可以使用该脚本执行任何 API,并在他们不知道的情况下代表用户执行操作。 - cookie能跨域吗?如何设置?
在同一域名下,不同页面之间可以共享 Cookie;而在不同域名下,默认情况下是不能共享 Cookie 的。 跨域设置 Cookie 需要满足以下条件:
- 响应头设置 Access-Control-Allow-Credentials: true:表示允许发送 Cookie,需要与前端请求中的 withCredentials 字段配合使用。
- 响应头设置 Access-Control-Allow-Origin: 请求域名,表示允许该域名下的请求访问资源,而不仅仅是同一域名下的请求。
- 前端请求中需要设置 withCredentials: true:表示允许发送 Cookie,需要与响应头中的 Access-Control-Allow-Credentials 配合使用。
//设置 Cookie 并允许跨域访问可以这样实现:
//后端响应头
Access-Control-Allow-Origin: http://api.example.com
//告诉浏览器,“我允许哪些域名的网页来访问我的资源”。
Access-Control-Allow-Credentials: true
//前端请求
fetch('http://api.example.com/data', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
})
- cookie, sessionStorage, localStorage区别
相同点:都存储在客户端
不同点:
1.存储大小
cookie数据大小不能超过4k。
sessionStorage和localStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。
2.有效时间
localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据;
sessionStorage 数据在当前浏览器窗口关闭后自动删除。
cookie 设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
- 数据与服务器之间的交互方式
cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端
sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。
- cookie其它缺点:
安全性问题:由于在HTTP请求中的cookie是明文传递的(HTTPS不是),带来的安全性问题还是很大的。
网络负担:我们知道cookie会被附加在每个HTTP请求中,在HttpRequest 和HttpResponse的header中都是要被传输的,所以无形中增加了一些不必要的流量损失。
操作文件
在HTML表单中,可以上传文件的唯一控件就是< input type=“file”>
注意:当一个表单包含< input type=“file”>时,表单的enctype必须指定为multipart/form-data,method必须指定为post,浏览器才能正确编码并以multipart/form-data格式发送表单的数据。
出于安全考虑,浏览器只允许用户点击< input type=“file”>来选择本地文件,用JavaScript对< input type=“file”>的value赋值是没有任何效果的。当用户选择了上传某个文件后,JavaScript也无法获得该文件的真实路径
通常,上传的文件都由后台服务器处理,JavaScript可以在提交表单时对文件扩展名做检查,以便防止用户上传无效格式的文件:
var f = document.getElementById('test-file-upload');
var filename = f.value; // 'C:\fakepath\test.png'
if (!filename || !(filename.endsWith('.jpg') || filename.endsWith('.png') || filename.endsWith('.gif'))) {
alert('Can only upload image file.');
return false;
}
随着HTML5的普及,新增的File API允许JavaScript读取文件内容,获得更多的文件信息。
HTML5的File API提供了File和FileReader两个主要对象,可以获得文件信息并读取文件。
下面的例子演示了如何读取用户选取的图片文件,并在一个< div>中预览图像:
var
fileInput = document.getElementById('test-image-file'),
info = document.getElementById('test-file-info'),
preview = document.getElementById('test-image-preview');
// 监听change事件:
fileInput.addEventListener('change', function () {
// 清除背景图片:
preview.style.backgroundImage = '';
// 检查文件是否选择:
if (!fileInput.value) {
info.innerHTML = '没有选择文件';
return;
}
// 获取File引用:
var file = fileInput.files[0];
// 获取File信息:
info.innerHTML = '文件: ' + file.name + '<br>' +
'大小: ' + file.size + '<br>' +
'修改: ' + file.lastModifiedDate;
if (file.type !== 'image/jpeg' && file.type !== 'image/png' && file.type !== 'image/gif') {
alert('不是有效的图片文件!');
return;
}
// 读取文件:
var reader = new FileReader();// FileReader() 返回一个新构造的FileReader。
//当 FileReader 读取文件的方式为
//readAsArrayBuffer,
//readAsBinaryString,
//readAsDataURL 或者 readAsText 的时候,会触发一个 load 事件。从而可以使用 FileReader.onload 属性对该事件进行处理。
reader.onload = function(e) {
var data = e.target.result;
// 'data:image/jpeg;base64,/9j/4AAQSk...(base64编码)...'
preview.style.backgroundImage = 'url(' + data + ')';
};
//
// 以DataURL的形式读取文件:
reader.readAsDataURL(file);
//readAsDataURL 方法会读取指定的 Blob 或 File 对象。
//读取操作完成的时候,readyState 会变成已完成DONE,并触发 loadend 事件,
//result属性将包含一个data:URL格式的字符串(base64编码)以表示所读取文件的内容。
});
Canvas
Canvas是HTML5新增的组件,可以用JavaScript在上面绘制各种图表、动画等
一个Canvas定义了一个指定尺寸的矩形框,在这个范围内我们可以随意绘制:
<canvas id="test-canvas" width="300" height="200"></canvas>
由于浏览器对HTML5标准支持不一致,
所以,通常在<canvas>内部添加一些说明性HTML代码,
如果浏览器支持Canvas,它将忽略<canvas>内部的HTML,
如果浏览器不支持Canvas,它将显示<canvas>内部的HTML:
<canvas id="test-stock" width="300" height="200">
<p>Current Price: 25.51</p>
</canvas>
在使用Canvas前,用canvas.getContext来测试浏览器是否支持Canvas:
<!-- HTML代码 -->
<canvas id="test-canvas" width="200" heigth="100">
<p>你的浏览器不支持Canvas</p>
</canvas>
var canvas = document.getElementById('test-canvas');
if (canvas.getContext) {
console.log('你的浏览器支持Canvas!');
} else {
console.log('你的浏览器不支持Canvas!');
}
getContext('2d')方法让我们拿到一个CanvasRenderingContext2D对象,所有的绘图操作都需要通过这个对象完成。
var ctx = canvas.getContext('2d');
如果需要绘制3D怎么办?HTML5还有一个WebGL规范,允许在Canvas中绘制3D图形:
gl = canvas.getContext("webgl");
一:绘制形状
先了解一下Canvas的坐标系统:
以像素为单位,所以每个点都是非负整数。
ctx.clearRect(0, 0, 200, 200); // 擦除(0,0)位置大小为200x200的矩形,擦除的意思是把该区域变为透明
ctx.fillStyle = '#dddddd'; // 设置颜色
ctx.fillRect(10, 10, 130, 130); // 把(10,10)位置大小为130x130的矩形涂色
// 利用Path绘制复杂路径:
var path=new Path2D();//创建对象
path.arc(75, 75, 50, 0, Math.PI*2, true);
path.moveTo(110,75);//画笔移到新坐标
path.arc(75, 75, 35, 0, Math.PI, false);
path.moveTo(65, 65);
path.arc(60, 65, 5, 0, Math.PI*2, true);
path.moveTo(95, 65);
path.arc(90, 65, 5, 0, Math.PI*2, true);
ctx.strokeStyle = '#0000ff';//用颜色画笔
ctx.stroke(path);//
stroke() 方法会实际地绘制出通过 moveTo() 和 lineTo() 方法定义的路径。默认颜色是黑色。
lineTo为终点 moveTo为起点
二、绘制文本
绘制文本就是在指定的位置输出文本,可以设置文本的字体、样式、阴影等,与CSS完全一致
还可以实现动画、缩放、各种滤镜和像素转换等高级操作。
如果要实现非常复杂的操作,考虑以下优化方案:
1·通过创建一个不可见的Canvas来绘图,然后将最终绘制结果复制到页面的可见Canvas中;
2·尽量使用整数坐标而不是浮点数;
3·可以创建多个重叠的Canvas绘制不同的层,而不是在一个Canvas中绘制非常复杂的图;
4·背景图片如果不变可以直接用<img>标签并放到最底层。
异常处理
- throw
throw
抛出异常信息,程序也会终止执行
throw
后面跟的是错误提示信息
Error
对象配合throw
使用,能够设置更详细的错误信息
function counter(x, y) {
if(!x || !y) {
// throw '参数不能为空!';
throw new Error('参数不能为空!');
}
return x + y;
}
counter();
- try…catch
try...catch
用于捕获错误信息
将预估可能发生错误的代码写在try
代码段中
如果try
代码段中出现错误后,会执行catch
代码段,并截获到错误信息
function foo() {
try {
// 查找 DOM 节点
var p = docunent.querySelector('p');
} catch(error) {
// try 代码段中执行有错误时,会执行 catch 代码段
// 查看错误信息
console.log(error.message);
// 终止代码继续执行
return;
}
// 改变文本样式
p.style.color = 'red';
}
foo();
前端模块化机制
- ES Module 和 commonjs 的区别?
● ES6 Module是值的引入,CommonJS是值的拷贝
● ES6 Module是编译的时候输出接口,CommonJS是运行加载
● ES6 Module是异步加载,CommonJS是同步加载 - ES6 Module和CommonJS模块的共同点:
● CommonJS和ES6 Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变。 - IIFE