Ned
深入理解JavaScript异步编程:异步原理、常见模式和最佳实践
1. 异步编程的实现方式?JavaScript中的异步机制可以分为以下几种:回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。2. setTimeout、Promise、Async/Await 的区别(1)setTimeoutconsole.log('script start') //1. 打印 script start
setTimeout(function(){
console.log('settimeout') // 4. 打印 settimeout
}) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end') //3. 打印 script start
// 输出顺序:script start->script end->settimeout(2)PromisePromise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout当JS主线程执行到Promise对象时:promise1.then() 的回调就是一个 taskpromise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queuepromise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况(3)async/awaitasync function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 输出顺序:script start->async1 start->async2->script end->async1 endasync 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。例如:func1().then(res => {
console.log(res); // 30
})func1的运行结果其实就是一个Promise对象。因此也可以使用then来处理后续逻辑。func1().then(res => {
console.log(res); // 30
})await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。3. 对Promise的理解Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。(1)Promise的实例有三个状态:Pending(进行中)Resolved(已完成)Rejected(已拒绝)当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。(2)Promise的实例有两个过程:pending -> fulfilled : Resolved(已完成)pending -> rejected:Rejected(已拒绝)注意:一旦从进行状态变成为其他状态就永远不能更改状态了。Promise的特点:对象的状态不受外界影响。promise对象代表一个异步操作,有三种状态,pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是promise这个名字的由来——“承诺”;一旦状态改变就不会再变,任何时候都可以得到这个结果。promise对象的状态改变,只有两种可能:从pending变为fulfilled,从pending变为rejected。这时就称为resolved(已定型)。如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。Promise的缺点:无法取消Promise,一旦新建它就会立即执行,无法中途取消。如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。总结: Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。注意: 在构造 Promise 的时候,构造函数内部的代码是立即执行的4. Promise的基本用法(1)创建Promise对象Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});一般情况下都会使用new Promise()来创建promise对象,但是也可以使用promise.resolve和promise.reject这两个方法:Promise.resolvePromise.resolve(value)的返回值也是一个promise对象,可以对返回值进行.then调用,代码如下:Promise.resolve(11).then(function(value){
console.log(value); // 打印出11
});resolve(11)代码中,会让promise对象进入确定(resolve状态),并将参数11传递给后面的then所指定的onFulfilled 函数;创建promise对象可以使用new Promise的形式创建对象,也可以使用Promise.resolve(value)的形式创建promise对象;Promise.rejectPromise.reject 也是new Promise的快捷形式,也创建一个promise对象。代码如下:Promise.reject(new Error(“我错了,请原谅俺!!”));就是下面的代码new Promise的简单形式:new Promise(function(resolve,reject){
reject(new Error("我错了!"));
});下面是使用resolve方法和reject方法:function testPromise(ready) {
return new Promise(function(resolve,reject){
if(ready) {
resolve("hello world");
}else {
reject("No thanks");
}
});
};
// 方法调用
testPromise(true).then(function(msg){
console.log(msg);
},function(error){
console.log(error);
});上面的代码的含义是给testPromise方法传递一个参数,返回一个promise对象,如果为true的话,那么调用promise对象中的resolve()方法,并且把其中的参数传递给后面的then第一个函数内,因此打印出 “hello world”, 如果为false的话,会调用promise对象中的reject()方法,则会进入then的第二个函数内,会打印No thanks;(2)Promise方法Promise有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。then()当Promise执行的内容符合成功条件时,调用resolve函数,失败就调用reject函数。Promise创建完了,那该如何调用呢?promise.then(function(value) {
// success
}, function(error) {
// failure
});then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。 then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。当要写有顺序的异步事件时,需要串行时,可以这样写:let promise = new Promise((resolve,reject)=>{
ajax('first').success(function(res){
resolve(res);
})
})
promise.then(res=>{
return new Promise((resovle,reject)=>{
ajax('second').success(function(res){
resolve(res)
})
})
}).then(res=>{
return new Promise((resovle,reject)=>{
ajax('second').success(function(res){
resolve(res)
})
})
}).then(res=>{
})那当要写的事件没有顺序或者关系时,还如何写呢?可以使用all 方法来解决。2. catch()Promise对象除了有then方法,还有一个catch方法,该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。p.then((data) => {
console.log('resolved',data);
},(err) => {
console.log('rejected',err);
}
);
p.then((data) => {
console.log('resolved',data);
}).catch((err) => {
console.log('rejected',err);
});3. all()all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。javascript
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(1);
},2000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},1000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},3000)
});
Promise.all([promise1,promise2,promise3]).then(res=>{
console.log(res);
//结果为:[1,2,3]
})调用all方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个promise对象resolve执行时的值。(4)race()race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected。let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
reject(1);
},2000)
});
let promise2 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(2);
},1000)
});
let promise3 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(3);
},3000)
});
Promise.race([promise1,promise2,promise3]).then(res=>{
console.log(res);
//结果:2
},rej=>{
console.log(rej)};
)那么race方法有什么实际作用呢?当要做一件事,超过多长时间就不做了,可以用这个方法来解决:Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})5. finally()finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。server.listen(port)
.then(function () {
// ...
})
.finally(server.stop);finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally本质上是then方法的特例:promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。5. Promise解决了什么问题在工作中经常会碰到这样一个需求,比如我使用ajax发一个A请求后,成功后拿到数据,需要把数据传给B请求;那么需要如下编写代码:let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
fs.readFile(data,'utf8',function(err,data){
console.log(data)
})
})
})上面的代码有如下缺点:后一个请求需要依赖于前一个请求成功后,将数据往下传递,会导致多个ajax请求嵌套的情况,代码不够直观。如果前后两个请求不需要传递参数的情况下,那么后一个请求也需要前一个请求成功后再执行下一步操作,这种情况下,那么也需要如上编写代码,导致代码不够直观。Promise出现之后,代码变成这样:let fs = require('fs')
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,'utf8',function(error,data){
error && reject(error)
resolve(data)
})
})
}
read('./a.txt').then(data=>{
return read(data)
}).then(data=>{
return read(data)
}).then(data=>{
console.log(data)
})这样代码看起了就简洁了很多,解决了地狱回调的问题。6. Promise.all和Promise.race的区别的使用场景(1)Promise.all Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。(2)Promise.race顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})7. 对async/await 的理解async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定await只能出现在asnyc函数中,先来看看async函数返回了什么:async function testAsy(){
return 'hello world';
}
let result = testAsy();
console.log(result)所以,async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样:async function testAsy(){
return 'hello world'
}
let result = testAsy()
console.log(result)
result.then(v=>{
console.log(v) // hello world
})那如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)。联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。注意:Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。8. await 到底在等啥?await 在等待什么呢? 一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行:function getSomething() {
return "something";
}
async function testAsync() {
return Promise.resolve("hello async");
}
async function test() {
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2);
}
test();await 表达式的运算结果取决于它等的是什么。如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。来看一个例子:function testAsy(x){
return new Promise(resolve=>{setTimeout(() => {
resolve(x);
}, 3000)
}
)
}
async function testAwt(){
let result = await testAsy('hello world');
console.log(result); // 3秒钟之后出现hello world
console.log('cuger') // 3秒钟之后出现cug
}
testAwt();
console.log('cug') //立即输出cug这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。await暂停当前async的执行,所以'cug''最先输出,hello world'和‘cuger’是3秒钟后同时出现的。9. async/await的优势单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。仍然用 setTimeout 来模拟异步操作:/**
* 传入参数 n,表示这个函数执行的时间(毫秒)
* 执行的结果是 n + 200,这个值将用于下一步骤
*/
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}现在用 Promise 方式来实现这三个步骤的处理:function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms输出结果 result 是 step3() 的参数 700 + 200 = 900。doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。如果用 async/await 来实现呢,会是这样:async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样10. async/await对比Promise的优势代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。11. async/await 如何捕获异常async function fn(){
try{
let a = await Promise.reject('error')
}catch(error){
console.log(error)
}
}12. 并发与并行的区别?并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。13. 什么是回调函数?回调函数有什么缺点?如何解决回调地狱问题?以下代码就是一个回调函数的例子:ajax(url, () => {
// 处理逻辑
})回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,可能会有如下代码:ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})以上代码看起来不利于阅读和维护,当然,也可以把函数分开来写:function firstAjax() {
ajax(url1, () => {
// 处理逻辑
secondAjax()
})
}
function secondAjax() {
ajax(url2, () => {
// 处理逻辑
})
}
ajax(url, () => {
// 处理逻辑
firstAjax()
})以上的代码虽然看上去利于阅读了,但是还是没有解决根本问题。回调地狱的根本问题就是:嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身嵌套函数一多,就很难处理错误当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return。14. setTimeout、setInterval、requestAnimationFrame 各有什么特点?异步编程当然少不了定时器了,常见的定时器函数有 setTimeout、setInterval、requestAnimationFrame。最常用的是setTimeout,很多人认为 setTimeout 是延时多久,那就应该是多久后执行。其实这个观点是错误的,因为 JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行。当然了,可以通过代码去修正 setTimeout,从而使定时器相对准确:let period = 60 * 1000 * 60 * 2
let startTime = new Date().getTime()
let count = 0
let end = new Date().getTime() + period
let interval = 1000
let currentInterval = interval
function loop() {
count++
// 代码执行所消耗的时间
let offset = new Date().getTime() - (startTime + count * interval);
let diff = end - new Date().getTime()
let h = Math.floor(diff / (60 * 1000 * 60))
let hdiff = diff % (60 * 1000 * 60)
let m = Math.floor(hdiff / (60 * 1000))
let mdiff = hdiff % (60 * 1000)
let s = mdiff / (1000)
let sCeil = Math.ceil(s)
let sFloor = Math.floor(s)
// 得到下一次循环所消耗的时间
currentInterval = interval - offset
console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval)
setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)接下来看 setInterval,其实这个函数作用和 setTimeout 基本一致,只是该函数是每隔一段时间执行一次回调函数。通常来说不建议使用 setInterval。第一,它和 setTimeout 一样,不能保证在预期的时间执行任务。第二,它存在执行累积的问题,请看以下伪代码function demo() {
setInterval(function(){
console.log(2)
},1000)
sleep(2000)
}
demo()以上代码在浏览器环境中,如果定时器执行过程中出现了耗时操作,多个回调函数会在耗时操作结束以后同时执行,这样可能就会带来性能上的问题。如果有循环定时器的需求,其实完全可以通过 requestAnimationFrame 来实现:function setInterval(callback, interval) {
let timer
const now = Date.now
let startTime = now()
let endTime = startTime
const loop = () => {
timer = window.requestAnimationFrame(loop)
endTime = now()
if (endTime - startTime >= interval) {
startTime = endTime = now()
callback(timer)
}
}
timer = window.requestAnimationFrame(loop)
return timer
}
let a = 0
setInterval(timer => {
console.log(1)
a++
if (a === 3) cancelAnimationFrame(timer)
}, 1000)首先 requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,当然你也可以通过该函数来实现 setTimeout。
Ned
解密JavaScript数组的神奇世界(必备操作指南)
冒泡排序(相邻)的两个数进行比较,符合条件,交换位置。
冒泡排序:核心-相邻的两个数进行比较,符合条件,交换位置。
小到大
从前到后
9 6 6 4 2
6 9 4 2 4
15 4 2 6
4 2 9
2 15
//声明数组
var arr = [9,6,15,4,2];
//轮数
function fnBubbleSortFromSmallToBig(arr){
for(var i = 1;i < arr.length;i ++){
//从数组中取元素---遍历
for(var j = 0;j < arr.length - i;j ++){
if(arr[j] > arr[j + 1]){
var t = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = t;
}
}
}
return arr;
}
function fnBubbleSortFromBigToSmall(arr){
for(var i = 1;i < arr.length;i ++){
//从数组中取元素---遍历
for(var j = 0;j < arr.length - i;j ++){
if(arr[j] < arr[j + 1]){
var t = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = t;
}
}
}
return arr;
}选择排序依次取一个元素 与 剩下所有元素 进行比较,符合条件,交换位置 。 数组常用方法 选择排序:核心-依次取一个元素与剩下所有的元素进行比较,符合条件,交换位置。
小到大
从前到后
9 6 6 4 2
6 9 4 2 4
15 4 2 6
4 2 9
2 15
function fnSelectFromSmallToBig(arr){
//遍历
for(var i = 0;i < arr.length - 1;i ++){
//遍历
for(var j = i + 1;j < arr.length;j ++){
if(arr[i] > arr[j]){
var t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
}
return arr;
}
function fnSelectFromBigToSmall(arr){
//遍历
for(var i = 0;i < arr.length - 1;i ++){
//遍历
for(var j = i + 1;j < arr.length;j ++){
if(arr[i] < arr[j]){
var t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
}
return arr;
}数组常用方法增unshift(元素,元素,……) : 前增作用:在数组前面添加元素。返回值: 返回新增后数组的长度是否影响原数组: 是//声明一个数组
var arr = [5,6,7,8,9];
//在前面新增一个元素
console.log(arr.unshift(true,[1,2,3],false)); //8
console.log(arr); //[true,[1,2,3],false,5,6,7,8,9]2. push(元素,元素,……) :后增作用:在数组后面添加元素。返回值: 返回新增后数组的长度是否影响原数组: 是//声明一个数组
var arr = [5,6,7,8,9];
//在前面新增一个元素
console.log(arr.push(true,[1,2,3],false)); //8
console.log(arr); //[5,6,7,8,9,true,[1,2,3],false]删shift() : 前删作用:删除数组中第1个元素返回值: 被删除的元素是否影响原数组: 是//声明一个数组
var arr = [5,6,7,8,9];
console.log(arr.shift()); //5
console.log(arr); //[6,7,8,9]
//声明一个数组
var arr = [5,6,7,8,9];
// console.log(arr.shift()); //5
// console.log(arr); //[6,7,8,9]
//如何删除全部?
while(arr.length){ //0
arr.shift();
}
console.log(arr);2. pop() : 后删作用:删除数组中最后一个元素返回值: 被删除的元素是否影响原数组: 是 //声明一个数组
var arr = [5,6,7,8,9];
console.log(arr.pop()); //9
console.log(arr); //[5, 6,7,8]改splice(start,delLength,newEle,newEle,……)start: 从哪个下标位置开始 delLenght: 删除几个元素 newEle : 新元素作用:在数组的任意位置进行增、删、改的操作。返回值: 被删除的元素数组是否影响原数组: 是//声明一个数组
var arr = [5,6,7,8,9];
console.log(arr.splice(1)); //[6, 7, 8, 9]
console.log(arr); //[5]
//声明一个数组
var arr = [5,6,7,8,9];
console.log(arr.splice(1,2)); //[6, 7]
console.log(arr); //[5,8,9]
//声明一个数组
var arr = [5,6,7,8,9];
console.log(arr.splice(1,2,true,[1,2],false)); //[6, 7]
console.log(arr); //[5,true,[1,2],false,8,9]
//声明一个数组
var arr = [5,6,7,8,9];
console.log(arr.splice(1,0,true,[1,2],false)); //[]
console.log(arr); //[5,true,[1,2],false,6,7,8,9]截slice(start,end)start : 从哪里开始(包含) end : 到哪里结束(不包含)作用:截取数组中指定范围的元素。返回值: 被截取到的元素数组是否影响原数组: 否//声明一个数组
var arr = [5,6,7,8,9];
console.log(arr.slice(1)); //[6,7,8,9]
console.log(arr); //[5,6,7,8,9]
//声明一个数组
var arr = [5,6,7,8,9];
console.log(arr.slice(1,4)); //[6,7,8]
console.log(arr); //[5,6,7,8,9]
// //声明一个数组
// var arr = [5,6,7,8,9];
// console.log(arr.slice(4,1)); //[]
// console.log(arr); //[5,6,7,8,9]
//声明一个数组
var arr = [5,6,7,8,9];
console.log(arr.slice(-4,-1)); // [6, 7, 8]
console.log(arr); //[5,6,7,8,9]拼concat(newEle,newEle,……)作用:将新元素拼接到指定数组的末尾。(如果新元素是一个数组,则去掉最外层的[],将里面的内容进行拼接)返回值: 拼接后的新数组是否影响原数组: 否//声明一个数组
var arr = [5,6,7,8,9];
console.log(arr.concat(-4,[1,[2,3]],-1)); //[5, 6, 7, 8, 9, -4, 1, [2,3], -1]
console.log(arr); //[5,6,7,8,9]排reverse() : 逆序排作用:将数组元素逆序存放返回值: 逆序后的数组是否影响原数组: 是var arr = [5,6,7,8,9];
console.log(arr.reverse()); //[9,8,7,6,5]
console.log(arr); //[9,8,7,6,5]2. sort() : 按编码排作用:将数组中的元素按字符编码从小到大排序返回值: 排序后的数组是否影响原数组: 是sort(function(a,b){return a - b;}) : 按数字从小到大排序 sort(function(a,b){return b - a;}) : 按数字从大到小排序var arr = [3,2,10,1,100,20];
console.log(arr.sort()); //[1,10,100,2,20,3]
console.log(arr); //[1, 10, 100, 2, 20, 3]转toString() : (面试题:不是数组的方法,是Object对象的方法,数组继承到的方法)数字.toString(2-36) : 将数字转为指定进制的字符串var i = 10;
console.log(i.toString(2)); //1010
console.log(i.toString(8)); //12
console.log(i.toString(16)); //a数组.toString() : 将数组转为字符串var arr = [1,2,3,4];
console.log(arr.toString()); //'1,2,3,4'
console.log(arr); //[1,2,3,4]join('连接符')作用:将数组转为以指定连接符连接成的字符串。返回值: 字符串是否影响原数组: 否var arr = [1,2,3,4];
console.log(arr.join('+')); //'1+2+3+4'
console.log(arr); //[1,2,3,4]ES5新增(都不会影响原数组)indexOf(元素,start): 查找元素在数组中第一次出现的下标位置,如果没有,则返回-1 var arr = [1,2,3,1,2,3,2,4,2,1];
console.log(arr.indexOf(2,2)); //4
console.log(arr.indexOf(5)); // -1lastIndexOf(元素,start) : 查找元素在数组中从右向左查找第一次出现的下标位置,如果没有找到,返回 -1var arr = [1,2,3,1,2,3,2,4,2,1];
console.log(arr.indexOf(2,2)); //4
console.log(arr.indexOf(5)); // -1
console.log(arr.lastIndexOf(2)); //8
console.log(arr.lastIndexOf(2,2)); //1
console.log(arr.lastIndexOf(2,5)); //4forEach(function(value,index,array){}) : 遍历数组map(function(value,index,array){return ...}) : 遍历数组,返回数组some(function(value,index,array){return ...}) : 检测数组中每一个元素,如果有一个元素的条件返回true,则直接退出循环,返回true; 如果所有元素都返回false时,最终返回falseevery(function(value,index,array){return ...}) : 检测数组中每一个元素,如果有一个元素的条件返回false时,则直接退出循环,返回false。 如果所有元素都返回true时,最终返回true.filter(function(value,index,array){return ...}) : 过滤-条件,返回数组reduce(function(prev,next,index,array){return ...}) : 归并var arr = [99,100,89,59,98];
var sum = arr.reduce(function(prev,next){
console.log(prev,next);
// prev = 99 next = 100
//prev = 199 next = 89
// prev = 288 next = 59
//prev = 347 next = 98
//prev = 455
return prev + next;
})
alert(sum);
var sum = arr.reduce(function(prev,next){
console.log(prev,next);
// prev = 0 next = 99
//prev = 99 next = 89
return prev + next;
},0)
alert(sum);
Ned
用Three.js搞个炫酷的3D区块地图
常用的3D区块地图除了那个区块,还要满足波纹散点、渐变柱体、飞线、下钻上卷、视角适配等,点开我,这就安排!用Three.js给你搞一个!1.准备工作(1) 获取GeoJson阿里的地理数据工具:http://datav.aliyun.com/portal/school/atlas/area_selector#&lat=33.50475906922609&lng=104.32617187499999&zoom=4export function queryGeojson(adcode, isFull = true) {
return new Promise((resolve, reject) => {
fetch(
`https://geo.datav.aliyun.com/areas_v3/bound/geojson?code=${adcode + (isFull ? '_full' : '')}`
)
.then((res) => res.json())
.then((data) => {
console.log(data);
resolve(data);
})
.catch(async (err) => {
if (isFull) {
let res = await queryGeojson(adcode, false);
resolve(res);
} else {
reject();
}
});(2) 经纬度转墨卡托投影这里使用的是d3geo,有一些Geojson不走经纬度的标准,直接是墨卡托投影坐标,所以需要判断一下,在经纬度范围才对它进行墨卡托投影坐标转换import d3geo from './d3-geo.js';
let geoFun = d3geo.geoMercator().scale(180);
export const latlng2px = (pos) => {
if (pos[0] >= -180 && pos[0] <= 180 && pos[1] >= -90 && pos[1] <= 90) {
return geoFun(pos);
}
return pos;
};(3)获取区块基本信息遍历所有的坐标点,获取坐标范围,中心点,以及缩放值(该值用于下钻上卷的时候维持元素缩放比例)export function getGeoInfo(geojson) {
let bounding = {
minlat: Number.MAX_VALUE,
minlng: Number.MAX_VALUE,
maxlng: 0,
maxlat: 0
};
let centerM = {
lat: 0,
lng: 0
};
let len = 0;
//遍历点
geojson.features.forEach((a) => {
if (a.geometry.type == 'MultiPolygon') {
a.geometry.coordinates.forEach((b) => {
b.forEach((c) => {
c.forEach((item) => {
let pos = latlng2px(item);
//经纬度转墨卡托投影坐标换失败
if (Number.isNaN(pos[0]) || Number.isNaN(pos[1])) {
console.log(item, pos);
return;
}
centerM.lng += pos[0];
centerM.lat += pos[1];
if (pos[0] < bounding.minlng) {
bounding.minlng = pos[0];
}
if (pos[0] > bounding.maxlng) {
bounding.maxlng = pos[0];
}
if (pos[1] < bounding.minlat) {
bounding.minlat = pos[1];
}
if (pos[1] > bounding.maxlat) {
bounding.maxlat = pos[1];
}
len++;
});
});
});
} else {
a.geometry.coordinates.forEach((c) => {
c.forEach((item) => {
//...
});
});
}
});
centerM.lat = centerM.lat / len;
centerM.lng = centerM.lng / len;
//元素缩放比例
let scale = (bounding.maxlng - bounding.minlng) / 180;
return { bounding, centerM, scale };
}(4)渐变色/***
* 获取渐变色数组
* @param {string} startColor 开始颜色
* @param {string} endColor 结束颜色
* @param {number} step 颜色数量
*/
export function getGadientArray(startColor, endColor, step) {
let { red: startR, green: startG, blue: startB } = getColor(startColor);
let { red: endR, green: endG, blue: endB } = getColor(endColor);
let sR = (endR - startR) / step; //总差值
let sG = (endG - startG) / step;
let sB = (endB - startB) / step;
let colorArr = [];
for (let i = 0; i < step; i++) {
//计算每一步的hex值
let c =
'rgb(' +
parseInt(sR * i + startR) +
',' +
parseInt(sG * i + startG) +
',' +
parseInt(sB * i + startB) +
')';
// console.log('%c' + c, 'background:' + c);
colorArr.push(c);
}
return colorArr;
}2.画有热力的3D区块(1)基本行政区区块信息 if (this.adcode != options.adcode || !this.geoJson) {
//获取geojson
let res = await queryGeojson(options.adcode, true);
let res1 = await queryGeojson(options.adcode, false);
this.geoJson = res;
this.adcode = options.adcode;
this.geoJson1 = res1;
//获取区块信息
let info = getGeoInfo(this.geoJson1);
this.geoInfo = info;
//坐标范围
this.bounding = info.bounding;
//元素缩放比例
this.sizeScale = info.scale;
}(2)画出区块计算热力区块:生成热力颜色列表:渐变色 let colorList = getGadientArray(
options.regionStyle.colorList[0],
options.regionStyle.colorList[1],
this.colorNum
);数值分阶let minValue;//最小值
let maxValue;//最大值
let valueLen;//单位长度
if (options.data.length > 0) {
minValue = options.data[0].value;
maxValue = options.data[0].value;
options.data.forEach((item) => {
if (item.value < minValue) {
minValue = item.value;
}
if (item.value > maxValue) {
maxValue = item.value;
}
});
valueLen = (maxValue - minValue) / this.colorNum;
}根据区块值所在的区间取对应颜色值 //获取该区块热力值颜色
let regionIdx = options.data.findIndex((item) => item.name == regionName);
if (regionIdx >= 0) {
let regionData = options.data[regionIdx];
let cIdx = Math.floor((regionData.value - minValue) / valueLen);
cIdx = cIdx >= this.colorNum ? this.colorNum - 1 : cIdx;
regionColor = colorList[cIdx];
}loaderExturdeGeometry() {
let options = this.that;
//激活材质
this.activeRegionMat = getBasicMaterial(THREE, options.regionStyle.emphasisColor);
//区块组
this.mapGroup = new THREE.Group();
//ExturdeGeometry厚度设置
const extrudeSettings = {
depth: options.regionStyle.depth * this.sizeScale,
bevelEnabled: false
};
//区块边框线颜色
const lineM = new THREE.LineBasicMaterial({
color: options.regionStyle.borderColor,
linewidth: options.regionStyle.borderWidth
});
//...
for (let idx = 0; idx < this.geoJson.features.length; idx++) {
let a = this.geoJson.features[idx];
//...
//多区块的行政区
if (a.geometry.type == 'MultiPolygon') {
a.geometry.coordinates.forEach((b) => {
b.forEach((c) => {
op.c = c;
this.createRegion(op);
});
});
} else {
//单区块的行政区
a.geometry.coordinates.forEach((c) => {
op.c = c;
this.createRegion(op);
});
}
}
this.objGroup.add(this.mapGroup);
}(3)每个区块形状和线框区块形状使用的是Shape的ExtrudeGeometry,差不多就是有厚度的canvas图形createRegion({ c, extrudeSettings, lineM, regionName, regionColor, idx, regionIdx }) {
const shape = new THREE.Shape();
const points = [];
//遍历该区块所有点画出形状
let pos0 = latlng2px(c[0]);
shape.moveTo(...pos0);
let h = 0;
points.push(new THREE.Vector3(...pos0, h));
for (let i = 1; i < c.length; i++) {
let p = latlng2px(c[i]);
shape.lineTo(...p);
points.push(new THREE.Vector3(...p, h));
}
shape.lineTo(...pos0);
//添加区块形状
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
let material = getBasicMaterial(THREE, regionColor);
const mesh = new THREE.Mesh(geometry, material);
mesh.name = regionName;
mesh.IDX = regionIdx;
mesh.rotateX(Math.PI * 0.5);
//收集动作元素
this.actionElmts.push(mesh);
//添加边框
const lineGeo = new THREE.BufferGeometry().setFromPoints(points);
const line = new THREE.Line(lineGeo, lineM);
line.name = 'regionline-' + idx;
line.rotateX(Math.PI * 0.5);
line.position.y = 0.03 * this.sizeScale;
let group = new THREE.Group();
group.name = 'region-' + idx;
group.add(mesh, line);
this.mapGroup.add(group);
}注意:shape画出来的canvas形状基于经纬度的墨卡托投影坐标作为xy坐标是竖着的,记得要旋转90度避免动作检测的元素过多,还是规规矩矩收集要监听的元素(4) 悬浮激活区块这里需要存储原来的区块材质,赋值激活状态材质,还要根据悬浮区块计算位置与大小,显示提示文本 doMouseAction(isChange) {
const intersects = this.raycaster.intersectObjects(this.actionElmts, true);
let newActiveObj;
let options = this.that;
if (intersects.length > 0) {
newActiveObj = intersects[0].object;
}
if (
(this.activeObj && newActiveObj && this.activeObj.name != newActiveObj.name) ||
(!this.activeObj && newActiveObj)
) {
console.log('active', newActiveObj);
//删除旧的提示文本
if (this.tooltip) {
this.cleanObj(this.tooltip);
this.tooltip = null;
}
//还原旧的区块材质
if (this.regions && this.beforeMaterial) {
this.regions.forEach((elmt) => {
elmt.material = this.beforeMaterial;
});
}
//存储旧的区块材质
this.beforeMaterial = newActiveObj.material;
let regions = this.actionElmts.filter((item) => item.name == newActiveObj.name);
let regionIdx = newActiveObj.regionIdx;
let idx = newActiveObj.idx;
let regionName = newActiveObj.name;
//将区块材质设置成激活状态材质
if (regions?.length) {
let center = new THREE.Vector3();
regions.forEach((elmt) => {
elmt.material = this.activeRegionMat;
elmt.updateMatrixWorld();
let box = new THREE.Box3().setFromObject(elmt);
let c = box.getCenter(new THREE.Vector3());
center.x += c.x;
center.y += c.y;
center.z += c.z;
});
//计算中心点,创建提示文本
center.x = center.x / regions.length;
center.y = center.y / regions.length;
center.z = center.z / regions.length;
newActiveObj.updateMatrixWorld();
let objBox = new THREE.Box3().setFromObject(newActiveObj);
this.createToolTip(regionName, regionIdx, center, objBox.getSize());
}
this.regions = regions;
this.activeObj = newActiveObj;
}
//点击下钻
if (this.that.isDown && isChange && newActiveObj && this.activeObj) {
//点击后赋值子级地址编码和地址名称,重新渲染
let f = this.geoJson.features[this.activeObj.idx];
this.that.adcode = f.properties.adcode;
this.that.address = f.properties.name;
console.log('next region', this.that.adcode);
this.createChart(this.that);
}
}注意:这里不能直接用区块经纬度坐标中心点作为提示框的位置,因为我这里对区块地图做了缩放和视角适配处理,所以经纬度坐标早已物是人非,位置对不上的,只能实时根据THREE.Box3来计算(5)创建提示文本createToolTip(regionName, regionIdx, center, scale) {
let op = this.that;
let text;
let data;
//文本格式化替换
if (regionIdx >= 0) {
data = op.data[regionIdx];
text = op.tooltip.formatter;
} else {
text = '{name}';
}
if (text.indexOf('{name}') >= 0) {
text = text.replace('{name}', regionName);
}
if (text.indexOf('{value}') >= 0) {
text = text.replace('{value}', data.value);
}
let { mesh, canvas } = getTextSprite(
THREE,
text,
op.tooltip.fontSize * this.sizeScale,
op.tooltip.color,
op.tooltip.bg
);
let s = this.latlngScale / this.sizeScale;
//注意canvas精灵的大小要保持原始比例
mesh.scale.set(canvas.width * 0.01 * s, canvas.height * 0.01 * s);
let box = new THREE.Box3().setFromObject(mesh);
this.tooltip = mesh;
this.tooltip.position.set(center.x, center.y + scale.y + box.getSize().y, center.z);
this.scene.add(mesh);
}注意canvas文本精灵的大小要保持原始比例,并且要适配当前行政区范围,要对其进行元素缩放(6)使用区块地图export default {
//文本提示样式
tooltip: {
//字体颜色
color: 'rgb(255,255,255)',
//字体大小
fontSize: 10,
//
formatter: '{name}:{value}',
//背景颜色
bg: 'rgba(30, 144 ,255,0.5)'
},
regionStyle: {
//厚度
depth: 5,
//热力颜色
colorList: ['rgb(241, 238, 246)', 'rgb(4, 90, 141)'],
//默认颜色
color: 'rgb(241, 238, 246)',
//激活颜色
emphasisColor: 'rgb(37, 52, 148)',
//边框样式
borderColor: 'rgb(255,255,255)',
borderWidth: 1
},
//视角控制
viewControl: {
autoCamera: true,
height: 10,
width: 0.5,
depth: 2,
cameraPosX: 10,
cameraPosY: 181,
cameraPosZ: 116,
autoRotate: false,
rotateSpeed: 2000
},
//是否下钻
isDown: false,
//地址名称
address: mapJson.name,
//地址编码
adcode: mapJson.adcode,
//区块数据
data: data.map((item) => ({
name: item.name,
code: item.code,
value: parseInt(Math.random() * 180)
})),
}
var map = new RegionMap();
map.initThree(document.getElementById('map'));
map.createChart(mapOption);
window.map = map;我这里没有使用光照,因为一旦增加光照就会导致每个区块的颜色出现偏差,这样可能会出现不符合UI设计的样式,该区热力值颜色不匹配等问题。3.画散点(1) 热力散点散点数据情况 let min = op.data[0].value,
max = op.data[0].value;
op.data.forEach((item) => {
if (item.value < min) {
min = item.value;
}
if (item.value > max) {
max = item.value;
}
});
let len = max - min;
let unit = len / this.colorNum;半径范围大小 let size = op.itemStyle.maxRadius - op.itemStyle.minRadius || 1;获取散点大小 let r;
if (len == 0) {
r = op.itemStyle.minRadius * this.sizeScale;
} else {
r = ((item.value - min) / len) * size + op.itemStyle.minRadius;
r = r * this.sizeScale;
} createScatter(op, idx) {
//...
//热力颜色列表
let colorList = getGadientArray(
op.itemStyle.colorList[0],
op.itemStyle.colorList[1],
this.colorNum
);
for (let index = 0; index < op.data.length; index++) {
let item = op.data[index];
let pos = latlng2px([item.lng, item.lat]);
//检查散点是否在范围内
if (this.checkBounding(pos)) {
//获取热力颜色...
let cIdx = Math.floor((item.value - min) / unit);
cIdx = cIdx >= this.colorNum ? this.colorNum - 1 : cIdx;
let color = colorList[cIdx];
let c = getColor(color);
const material = getBasicMaterial(
THREE,
`rgba(${c.red},${c.green},${c.blue},${op.itemStyle.opacity})`
);
//...
//散点
let geometry = new THREE.CircleGeometry(r, 32);
let mesh = new THREE.Mesh(geometry, material);
mesh.name = 'scatter-' + idx + '-' + index;
mesh.rotateX(0.5 * Math.PI);
mesh.position.set(pos[0], 0, pos[1]);
this.scatterGroup.add(mesh);
//波纹圈
if (op.itemStyle.isCircle) {
const { material: circleMaterial } = this.getCircleMaterial(
op.itemStyle.maxRadius * 20 * this.sizeScale,
color
);
let circle = new THREE.Mesh(new THREE.CircleGeometry(r * 2, 32), circleMaterial);
circle.name = 'circle' + idx + '-' + index;
circle.rotateX(0.5 * Math.PI);
circle.position.set(pos[0], 0, pos[1]);
this.circleGroup.add(circle);
}
}
}
//避免深度冲突,加个高度
this.scatterGroup.position.y = 0.1 * this.sizeScale;
if (op.itemStyle.isCircle) {
this.circleGroup.position.y = 0.1 * this.sizeScale;
}
}注意,这里做了范围过滤,超出区块范围的散点就不画了checkBounding(pos) {
if (
pos[0] >= this.bounding.minlng &&
pos[0] <= this.bounding.maxlng &&
pos[1] >= this.bounding.minlat &&
pos[1] <= this.bounding.maxlat
) {
return true;
}
return false;
}(2) 波纹散点圈getCircleMaterial(radius, color) {
const canvas = document.createElement('canvas');
canvas.height = radius * 3.1;
canvas.width = radius * 3.1;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = color;
//画三个波纹圈
//外圈
ctx.lineWidth = radius * 0.2;
ctx.beginPath();
ctx.arc(canvas.width * 0.5, canvas.height * 0.5, radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
//中圈
ctx.lineWidth = radius * 0.1;
ctx.beginPath();
ctx.arc(canvas.width * 0.5, canvas.height * 0.5, radius * 1.3, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
//内圈
ctx.lineWidth = radius * 0.05;
ctx.beginPath();
ctx.arc(canvas.width * 0.5, canvas.height * 0.5, radius * 1.5, 0, 2 * Math.PI);
ctx.closePath();
ctx.stroke();
const map = new THREE.CanvasTexture(canvas);
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
let res = getColor(color);
const material = new THREE.MeshBasicMaterial({
map: map,
transparent: true,
color: new THREE.Color(`rgb(${res.red},${res.green},${res.blue})`),
opacity: 1,
// depthTest: false,
side: THREE.DoubleSide
});
return { material, canvas };
}(3) 波纹圈动起来 //散点波纹扩散
if (this.circleGroup?.children?.length > 0) {
this.circleGroup.children.forEach((elmt) => {
if (elmt.material.opacity <= 0) {
elmt.material.opacity = 1;
this.circleScale = 1;
} else {
//大小变大,透明度减小
elmt.material.opacity += -0.01;
this.circleScale += 0.0002;
}
elmt.scale.x = this.circleScale;
elmt.scale.y = this.circleScale;
});
}(4)赋值,使用{
name: 'scatter3D',
type: 'scatter3D',
//数据
data: mapJson.districts.map((item) => ({
name: item.name,
lat: item.center[1],
lng: item.center[0],
value: parseInt(Math.random() * 100)
})),
formatter: '{name}:{value}',
itemStyle: {
isCircle: true, //是否开启波纹圈
opacity: 0.8,//透明度
maxRadius: 5, //最大半径
minRadius: 1, //最小半径
//热力颜色
colorList: ['rgb(255, 255, 178)', 'rgb(189, 0, 38)']
}
}4.画柱体(1)柱体顶点着色器varying vec3 vNormal;
varying vec2 vUv;
void main()
{
vNormal = normal;
vUv=uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}(2)柱体片元着色器
uniform vec3 topColor;
uniform vec3 bottomColor;
varying vec2 vUv;
varying vec3 vNormal;
void main() {
//顶面
if(vNormal.y==1.0){
gl_FragColor = vec4(topColor, 1.0 );
}else if(vNormal.y==-1.0){//底面
gl_FragColor = vec4(bottomColor, 1.0 );
}else{//颜色混合形成渐变
gl_FragColor = vec4(mix(bottomColor,topColor,vUv.y), 1.0 );
}
}(3)创建渐变材质export function getGradientShaderMaterial(THREE, topColor, bottomColor) {
const uniforms = {
topColor: { value: new THREE.Color(getRgbColor(topColor)) },
bottomColor: { value: new THREE.Color(getRgbColor(bottomColor)) }
};
return new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: barShader,
side: THREE.DoubleSide
});
}
(4) 创建柱体这里需要计算柱体高度,过滤区块范围外的柱体 createBar(op, idx) {
//渐变材质
const material = getGradientShaderMaterial(
THREE,
op.itemStyle.topColor,
op.itemStyle.bottomColor
);
//数据整体情况
let min = op.data[0].value,
max = op.data[0].value;
op.data.forEach((item) => {
if (item.value < min) {
min = item.value;
}
if (item.value > max) {
max = item.value;
}
});
let len = max - min;
for (let index = 0; index < op.data.length; index++) {
let item = op.data[index];
let pos = latlng2px([item.lng, item.lat]);
//柱体范围过滤
if (this.checkBounding(pos)) {
//计算柱体高度
let h = (((item.value - min) / len) * op.itemStyle.maxHeight + op.itemStyle.minHeight) *
this.sizeScale;
let bar = new THREE.BoxGeometry(
op.itemStyle.barWidth * this.sizeScale,
h,
op.itemStyle.barWidth * this.sizeScale
);
let barMesh = new THREE.Mesh(bar, material);
barMesh.name = 'bar-' + idx + '-' + index;
barMesh.position.set(pos[0], 0.5 * h, pos[1]);
this.barGroup.add(barMesh);
}
}
}(5)赋值使用 {
name: 'bar3D',
type: 'bar3D',
formatter: '{name}:{value}',
data: data.map((item) => ({
name: item.name,
code: item.code,
lat: item.center[1],
lng: item.center[0],
value: parseInt(Math.random() * 180)
})),
itemStyle: { //
maxHeight: 30,//柱体最大高度
minHeight: 1,//柱体最小高度
barWidth: 1,//柱体宽度
topColor: 'rgb(255, 255, 204)',//上方颜色
bottomColor: 'rgb(0, 104, 55)'//下方颜色
}
}5.画飞线(1)飞线着色器 uniform float time;
uniform vec3 colorA;
uniform vec3 colorB;
varying vec2 vUv;
void main() {
//根据时间和uv值控制颜色变化
vec3 color =vUv.x<time?colorB:colorA;
gl_FragColor = vec4(color,1.0);
}(2)创建飞线的材质export function getLineShaderMaterial(THREE, color, color1) {
const uniforms = {
time: { value: 0.0 },
colorA: { value: new THREE.Color(getRgbColor(color)) },
colorB: { value: new THREE.Color(getRgbColor(color1)) }
};
return new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: lineFShader,
side: THREE.DoubleSide,
transparent: true
});
}(3)创建飞线这里的飞线管道用的是QuadraticBezierCurve3贝塞尔曲线算出来的createLines(op, idx) {
const material = getLineShaderMaterial(THREE, op.itemStyle.color, op.itemStyle.runColor);
this.linesMaterial.push(material);
for (let index = 0; index < op.data.length; index++) {
let item = op.data[index];
let pos = latlng2px([item.fromlng, item.fromlat]);
let pos2 = latlng2px([item.tolng, item.tolat]);
//过滤飞线范围
if (this.checkBounding(pos) && this.checkBounding(pos2)) {
//中间点
let pos1 = latlng2px([
(item.fromlng + item.tolng) / 2,
(item.fromlat + item.tolat) / 2
]);
//贝塞尔曲线
const curve = new THREE.QuadraticBezierCurve3(
new THREE.Vector3(pos[0], 0, pos[1]),
new THREE.Vector3(pos1[0], op.itemStyle.lineHeight * this.sizeScale, pos1[1]),
new THREE.Vector3(pos2[0], 0, pos2[1])
);
const geometry = new THREE.TubeGeometry(
curve,
32,
op.itemStyle.lineWidth * this.sizeScale,
8,
false
);
const line = new THREE.Mesh(geometry, material);
line.name = 'lines-' + idx + '-' + index;
this.linesGroup.add(line);
}
}
}(4)让飞线动起来给shader赋值,让飞线颜色动起来//飞线颜色变化
if (this.linesGroup?.children?.length > 0) {
if (this.lineTime >= 1.0) {
this.lineTime = 0.0;
} else {
this.lineTime += 0.005;
}
this.linesMaterial.forEach((m) => {
m.uniforms.time.value = this.lineTime;
});
}(5)赋值使用 {
name: 'lines3D',
type: 'lines3D',
formatter: '{name}:{value}',
data: mapJson.districts.map((item) => ({
fromlat: item.center[1],
fromlng: item.center[0],
tolat: mapJson.center[1],
tolng: mapJson.center[0]
})),
itemStyle: {
lineHeight: 20, //飞线中间点高度
color: '#00FFFF', //原始颜色
runColor: '#1E90FF', //变化颜色
lineWidth: 0.3 //线宽
}
}6.Github我这里的格式是模仿echarts配置项的,所以柱体,飞线,散点可以存在多个不同系列。https://github.com/xiaolidan00/my-three
Ned
JavaScript中的for循环你用对了吗?
循环结构循环结构 : 满足一定条件,重复执行一个动作或一段代码。循环思想(三要素)从哪里开始到哪里结束步长(步进)实现循环的语句whiledo whilefor当型循环循环初值;
while(循环条件){
语句组;
步长;
}直到型循环循环初值;
do{
语句组;
步长;
}while(循环条件);多功能循环for(循环初值;循环条件;步长){
语句组;
}案例输出10个HelloWorld<script>
输出10个HelloWorld
输出: alert() document.write() console.log()
10个:重复 循环 三要素:i = 1; i < 11 i ++
循环语法:
while
do while
for
//while
//循环初值
var i = 1;
while(i < 11){
console.log('HelloWorld');
i ++;
// i += 2;
}
//do while
var j = 1;
do{
console.log('HelloWorld');
j ++;
}while(j < 11);
//for
for(var k = 1;k < 11;k ++){
console.log('HelloWorld');
}
</script>输出1~100的奇数 <script>
输出1~100的奇数
输出: alert() document.write() console.log()
1~100 : 重复 循环 三要素: i = 1 i < 101 i ++ (i += 2)
奇数: 不能被2整除 一个条件 一个结果 单分支 if(){}
while
var i = 1;
//准备一个变量,存放结果
var str = '';
while(i < 101){
if(i % 2){
// document.write(i + ' '); //满足一次条件,就和页面交互一次
//优化后的语句
str += i + ' '; //'' + 1 str = '1' + 3
}
i ++;
}
将str 一次性添加到页面中
document.write(str);
console.log(str);
alert(str);
var i = 1,str = '';
do{
str += i + ' ';
i += 2;
}while(i < 101);
document.write(str);
var i = 1,str = '';
do{
if(i % 2){
str += i + ' ';
}
i ++;
}while(i < 101);
document.write(str);
document.write('<br>');
for(var i = 1,str = '';i < 101;i += 2){
str += i + ',';
}
document.write(str);
</script>输出m~n的整数 <script>
输出m~n的整数
输出
m~n prompt() parseInt()
if(m < n){
如果m = 1,n = 5 重复-循环-三要素 i = m; i <= n;i ++
}else{
如果m = 5,n = 1 重复-循环-三要素 i = m; i >= n;i --
}
//1. 准备两个变量
var m = parseInt(prompt('请输入一个整数:'));
var n = parseInt(prompt('请输入一个整数:'));
//2. 判断谁大谁小
if(m < n){
for(var i = m,str = '';i <= n;i ++){
str += i + ' ';
}
//输出结果
document.write(str);
}else{
for(var i = m,str = '';i >= n;i --){
str += i + ' ';
}
//输出结果
document.write(str);
}
</script>i. 求5!(阶乘:从1 乘到它本身) <script>
求5!(阶乘:从1 乘到它本身) 1 * 2 * 3 * 4 * 5
1. 重复乘法的操作,所以使用循环 三要素 : i = 1 i < 6 i ++
for(var i = 1,fac = 1;i < 6;i ++){
fac *= i; //fac = 1 * 1 * 2 * 3 * 4 * 5
}
alert(fac);
</script>解决猴子吃桃的问题(有一只猴子,还有一堆桃子,第一天的时候,吃了一堆桃子中的一半,没忍住,又多吃了一个;第二天的时候,又吃了剩下桃子中的一半,没忍住,又多吃了一个,以后每天如此,直到第10天的时候,只剩下了一个桃子,问第一天的时候有多少个桃子) <script>
解决猴子吃桃的问题(有一只猴子,还有一堆桃子,第一天的时候,吃了一堆桃子中的一半,没忍住,又多吃了一个;第二天的时候,又吃了剩下桃子中的一半,没忍住,又多吃了一个,以后每天如此,直到第10天的时候,只剩下了一个桃子,问第一天的时候有多少个桃子)
1. 第10天: 1 sum = 1
2. 每天是怎么吃的? 第9天: (sum + 1) * 2 4
3. 8 (4 + 1) * 2 10
7 6 5 4 3 2 1 天
重复天数, 循环 i = 9 i > 0 i --
for(var day = 9,sum = 1;day > 0;day --){
sum = (sum + 1) * 2
}
alert(sum);
</script>求1+2+3+……+100的和 <script>
求1+2+3+……+100的和
重复 + 循环 三要素 i = 1 i < 101 i ++
for(var i = 1,sum = 0;i < 5;i ++){
sum += i;
}
alert(sum);
</script>输出1-100中(7的倍数和带7的数除外)的数。 <script>
输出1-100中(7的倍数和带7的数除外)的数。
1-100 : 循环 i = 1; i < 101 i ++
7的倍数和带7的数除外:
!(i % 7 === 0 || parseInt(i / 10) === 7 || i % 10 === 7)
i % 7 && parseInt(i / 10) !== 7 && i % 10 !== 7
//result : 结果
for(var i = 1,result = '';i < 101;i ++){
if(!(i % 7 === 0 || parseInt(i / 10) === 7 || i % 10 === 7)){
result += i + ' ';
}
}
document.write(result);
</script>输出m至n的自然数中的偶数和与奇数和并统计偶数与奇数的个数分别是多少? <script>
输出m至n的自然数中的偶数和与奇数和并统计偶数与奇数的个数分别是多少?
1. m 至 n 用户输入两个数
2. 确保 m < n
if(m > n){
交换两个值
}
3. 实现循环
三要素: i = m; i <= n; i ++
4. 是否能被2整除 奇数 偶数 双分支
偶数和
奇数和
偶数的数量
奇数的数量
//1. 准备两个变量
var m = parseInt(prompt('请输入一个整数:'));
var n = parseInt(prompt('请输入一个整数:'));
//1.1 准备放置结果的变量
// even : 偶数
// sum : 和
// odd : 奇数
//count: 统计
var even_sum = 0;
var odd_sum = 0;
var even_count = 0;
var odd_count = 0;
//2. 确保m < n
if(m > n){
var t = m;
m = n;
n = t;
}
//3. 实现循环
for(var i = m;i <= n;i ++){
//4. 判断奇偶
if(i % 2){
//true : 奇数
odd_sum += i;
odd_count ++;
}else{
//false : 偶数
even_sum += i;
even_count ++;
}
}
//5. 输出结果
console.log('偶数和:' + even_sum + '\n奇数和:' + odd_sum + '\n偶数数量:' + even_count + '\n奇数数量:' + odd_count);
</script>入职薪水10K,每年涨幅5%,50年后工资多少? <script>
入职薪水10K,每年涨幅5%,50年后工资多少?
初值: sum = 10
i = 2 i < 51 i ++
sum = sum + sum * 0.05 sum += sum * 0.05
for(var sum = 10,i = 2;i < 51;i ++){
sum += sum * 0.05;
}
alert(sum);
</script>流程控制关键字continue : 跳出本次循环,直接进入一下次循环。break : 跳出循环。 <script>
for(var i = 1;i < 6;i ++){
// if(i == 2 || i == 4){
// continue; //当i = 2 或 i = 4 的时候,直接跑到下一次循环
// }
if(i !== 2 && i !== 4){
//console.log() : 每输出一次,自动换一行
console.log(i); // 1 3 5
}
}
//break : 退出循环
for(var i = 1;i < 6;i ++){
if(i === 3){
break;
}
}
//上面的循环结束后,才能执行到下面的语句,那么 什么时候结束的?3
console.log(i); //3
</script>循环的区别 while: 1. 当型循环,先判断条件,后执行语句 2. 当条件第一次为假时,一次也不执行。 3. 常用于循环次数不确定时 do while: 1. 直到型循环,先执行语句,后判断条件 2. 当 条件第一次为假时,至少执行一次循环 3. 常用于循环次数不确定时 for 1. 多功能循环(当型循环),先判断条件,后执行语句 2. 当条件第一次为假时,一次也不执行 3. 常用于循环次数确定时
Ned
探索异步交互:JavaScript AJAX 的全面指南
AJAXajax 全名 async javascript and XML是前后台交互的能力也就是我们客户端给服务端发送消息的工具,以及接受响应的工具是一个 默认异步 执行机制的功能XML与JSON:都是可以跨平台、跨语言的一种数据格式。JSON的格式:[] : 写在[]中的字符串,必须使用双引号1.原生: [‘a’,“b”]2.JSON: [“a”,“b”]{} : key 必须加双引号,value如果是字符串,必须加双引号1.原生:{ name : ‘张三’} {name : “张三”} { ‘name’: ‘张三’} { “name”: “张三”}2.JSON: { “name” : “张三” }在JSON的值中:不能出现 undefined/NaN/function/Infinity通过异步与服务器通信,将用户请求的数据通过回调函数返回,并利用javascript将数据动态的添加到页面中,且整个过程不需要重新加载整个页面就可以完成AJAX 的优势异步与服务器通信不需要刷新页面就可以更新数据减少服务器负担,实现前后端负载平衡缺点: 搜索引擎的支持度不够,因为数据都不在页面上,搜索引擎搜索不到缺点:没有历史记录缺点:存在风险AJAX 的使用在 js 中有内置的构造函数来创建 ajax 对象创建 ajax 对象以后,我们就使用 ajax 对象的方法去发送请求和接受响应创建一个 ajax 对象// 除IE6所有浏览器都支持的
const xhr = new XMLHttpRequest()
// IE6
const xhr = new ActiveXObject('Mricosoft.XMLHTTP')上面就是有了一个 ajax 对象我们就可以使用这个 xhr 对象来发送 ajax 请求了配置链接信息const xhr = new XMLHttpRequest()
// xhr 对象中的 open 方法是来配置请求信息的
// 第一个参数是本次请求的请求方式 get / post / put / ...
// 第二个参数是本次请求的 url
// 第三个参数是本次请求是否异步,默认 true 表示异步,false 表示同步
// xhr.open('请求方式', '请求地址', 是否异步)
xhr.open('get', './data.php')上面的代码执行完毕以后,本次请求的基本配置信息就写完了发送请求const xhr = new XMLHttpRequest()
xhr.open('get', './data.php')
// 使用 xhr 对象中的 send 方法来发送请求
xhr.send()上面代码是把配置好信息的 ajax 对象发送到服务端一个基本的 ajax 请求一个最基本的 ajax 请求就是上面三步但是光有上面的三个步骤,我们确实能把请求发送的到服务端如果服务端正常的话,响应也能回到客户端但是我们拿不到响应如果想拿到响应,我们有两个前提条件本次 HTTP 请求是成功的,也就是我们之前说的 http 状态码为 200 ~ 299ajax 对象也有自己的状态码,用来表示本次 ajax 请求中各个阶段ajax 状态码ajax 状态码 - xhr.readyState是用来表示一个 ajax 请求的全部过程中的某一个状态readyState === 0: 表示未初始化完成,也就是 open 方法还没有执行readyState === 1: 表示配置信息已经完成,也就是执行完 open 之后readyState === 2: 表示 send 方法已经执行完成readyState === 3: 表示正在解析响应内容readyState === 4: 表示响应内容已经解析完毕,可以在客户端使用了这个时候我们就会发现,当一个 ajax 请求的全部过程中,只有当 readyState === 4 的时候,我们才可以正常使用服务端给我们的数据所以,配合 http 状态码为 200 ~ 299一个 ajax 对象中有一个成员叫做 xhr.status这个成员就是记录本次请求的 http 状态码的两个条件都满足的时候,才是本次请求正常完成readyStateChange在 ajax 对象中有一个事件,叫做 readyStateChange 事件这个事件是专门用来监听 ajax 对象的 readyState 值改变的的行为也就是说只要 readyState 的值发生变化了,那么就会触发该事件所以我们就在这个事件中来监听 ajax 的 readyState 是不是到 4 了const xhr = new XMLHttpRequest()
xhr.open('get', './data.php')
xhr.send()
xhr.onreadyStateChange = function () {
// 每次 readyState 改变的时候都会触发该事件
// 我们就在这里判断 readyState 的值是不是到 4
// 并且 http 的状态码是不是 200 ~ 299
if (xhr.readyState === 4 && /^2\d{2}$/.test(xhr.status)) {
// 这里表示验证通过
// 我们就可以获取服务端给我们响应的内容了
}
}responseTextajax 对象中的 responseText 成员就是用来记录服务端给我们的响应体内容的所以我们就用这个成员来获取响应体内容就可以const xhr = new XMLHttpRequest()
xhr.open('get', './data.php')
xhr.send()
xhr.onreadyStateChange = function () {
if (xhr.readyState === 4 && /^2\d{2|$/.test(xhr.status)) {
// 我们在这里直接打印 xhr.responseText 来查看服务端给我们返回的内容
console.log(xhr.responseText)
}
}ajax的工作原理前后端交互,首先需要创建一个XMLHttpRequest对象,通过这个对象的open方法与服务器建立连接,通过send方法将请求发送给服务器,最后通过事件监听,将后端请求的数据通过回调函数返回给前端。使用 ajax 发送请求时携带参数我们使用 ajax 发送请求也是可以携带参数的参数就是和后台交互的时候给他的一些信息但是携带参数 get 和 post 两个方式还是有区别的发送一个带有参数的 get 请求get 请求的参数就直接在 url 后面进行拼接就可以const xhr = new XMLHttpRequest()
// 直接在地址后面加一个 ?,然后以 key=value 的形式传递
// 两个数据之间以 & 分割
xhr.open('get', './data.php?a=100&b=200')
xhr.send()这样服务端就能接受到两个参数一个是 a,值是 100一个是 b,值是 200发送一个带有参数的 post 请求post 请求的参数是携带在请求体中的,所以不需要再 url 后面拼接const xhr = new XMLHttpRequest()
xhr.open('get', './data.php')
// 如果是用 ajax 对象发送 post 请求,必须要先设置一下请求头中的 content-type
// 告诉一下服务端我给你的是一个什么样子的数据格式
xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded')
// 请求体直接再 send 的时候写在 () 里面就行
// 不需要问号,直接就是 'key=value&key=value' 的形式
xhr.send('a=100&b=200')application/x-www-form-urlencoded 表示的数据格式就是 key=value&key=valueget与post的区别?get传递速度快传递数据大小为1KB参数在url地址后面传递get以明文方式传递参数,所以不安全。get会留下历史记录post传递速度慢传递数据大小为 2MB参数在http协议的请求体中传递post传递参数相对安全。不会留下历史记录封装 AJAXajax 使用起来太麻烦,因为每次都要写很多的代码那么我们就封装一个 ajax 方法来让我们使用起来简单一些确定一下使用的方式因为有一些内容可以不传递,我们可以使用默认值,所以选择对象传递参数的方式// 使用的时候直接调用,传递一个对象就可以
ajax({
url: '', // 请求的地址
type: '', // 请求方式
async: '', // 是否异步
data: '', // 携带的参数
dataType: '', // 要不要执行 json.parse
success: function () {} // 成功以后执行的函数
})确定好使用方式以后,就开始书写封装函数Promise什么是Promise?ES6提出的异步编程解决方案.承诺的意思,是一个专门用来解决异步 回调地狱 的问题回调地狱,其实就是回调函数嵌套过多导致的当代码成为这个结构以后,已经没有维护的可能了所以我们要把代码写的更加的艺术一些promise使用语法// 检测机构
//resolve : 处理异步时成功的状态
//reject : 处理异步时失败的状态
new Promise((resolve,reject) => {
if(处理异步){
resolve([参数]);
}else{
reject([参数]);
}
})Promise原型对象的方法then(([参数]) => {}) : 当Promise对象返回resolve时,可以通过then方法执行后续的操作.catch([参数] => {}) : 当promise对象返回reject时,可以通过catch方法执行后续的操作.处理多个异步new Promise((resolve,reject) => {
if(处理异步){
resolve([参数]);
}else{
reject([参数]);
}
})
.then(() => {
return new Promise(() => {
})
})
.then(() => {
return new Promise(() => {
})
})
……
.then(() => {
})Promise的静态方法Promise.all() 将调用所有的promse对象,全部返回resolve时,该对象才返回resolve。如果有一个promise返回reject时,该对象返回rejectPromise中的三种状态resolved(fulfilled) : 成功状态pending : 进行中状态rejected : 失败状态这个时候,我们的代码已经改观了很多了基本已经可以维护了但是对于一个程序员来说,这个样子是不够的我们还需要更加的简化代码所以我们就需要用到一个 es7 的语法了叫做 async/awaitASYNC/AWAITasync/await 是一个 es7 的语法这个语法是 回调地狱的终极解决方案语法:async function fn() {
const res = await promise对象
}async 和 await 关键字注意: 需要配合的必须是 Promise对象注意:Promise 语法的调用方案意义:可以把异步代码写的看起来像同步代码async 关键字的用法直接书写在函数的前面,表示该函数是一个异步函数意义: 表示在该函数内可以使用 await 关键字await 关键字的用法必须书写在一个有async关键字的函数内await 后面等待的内容必须是一个promise对象本该使用then接收的结果,可以直接定义变量接收缺点await 只能捕获到promise成功的状态如果失败,会报错,终止程序继续执行解决方案1.使用 try catch语法语法: try { 执行代码 } catch(err) { 执行代码 }首先执行 try 里面的代码, 如果不报错, catch 的代码不执行了如果报错, 不会爆出错误, 不会终止程序执行, 而是执行 catch 的代码, 把错误信息给到 err 参数2. 改变封装Promise的思路让当前的 Promise 对象百分百成功,让成功和失败都按照 resolve 的形式来执行,只不过传递出去的参数, 记录一个表示成功或者失败的信息
Ned
JavaScript中的this指向:如何避免常见的this陷阱
this指向详解this出现在全局范围调用函数时,永远指向window (this所在的function没有被明确的对象调用时,this指向window)var Car = function() {
console.log(this); // window
console.log(this.Car==window.Car,Car==window.Car); // true true
}
Car(); <script>
1. this代表所在function被哪一个对象调用了,那么这个this就代表这个对象。
2. 如果没有明确的调用对象,则代表window
function fn(){
alert(this);
}
fn(); window //没有明确对象,所以代表
document.onclick = fn; document
var obj = {fn: fn}
obj.fn(); obj
'use strict';
a = 3;
alert(a);
function fn(){
'use strict';
alert(this); undefined
}
fn();
</script>2. this出现在函数作用域严格模式中,永远不会指向window函数中使用ES5的严格模式‘use strict',this为undefinedfunction car() {
'use strict'
console.log(this); // undefined
}
car();3. 当某个函数为对象的一个属性时,在这个函数内部this指向这个对象var car = {
name:'丰田',
run() {
console.log(this); // {name: "丰田", run: ƒ}
}
}4. this出现在构造函数中,指向构造函数新创建的对象var Car = function(name) {
this.name = name;
console.log(this); // Car {name: "亚洲龙"}
// Car {name: "汉兰达"}
}
var myCar_1 = new Car('亚洲龙');
var myCar_2 = new Car('汉兰达');5. 当一个元素被绑定事件处理函数时,this指向被点击的这个元素var btn = document.querySelector('button');
btn.onclick = function() {
console.log(this); // <button>this</button>
}6. this出现在箭头函数中时,this和父级作用域的this指向相同const obj = {
Car() {
setTimeout(function() {
setTimeout(function() {
console.log(this); // window
})
setTimeout(()=>{
console.log(this); // window
})
})
setTimeout(() => {
setTimeout(function() {
console.log(this); // window
})
setTimeout(()=>{
console.log(this); // obj
})
})
}
}
obj.Car();强行改变 this 指向修改上下文中的this指向方法 (函数的方法,改变的是函数内部的this指向)call(对象,参数1,参数2,……) :返回对象后,这个函数立即运行apply(对象,数组或arguments) : 返回对象后,这个函数立即运行bind(对象,参数1,参数2,……) : 返回函数 <script>
var div = document.querySelector('#box');
var inp = document.querySelector('#txt');
document.onclick = function(){
alert(this); //document
setTimeout(function(){
alert(this); //window,想让this指向document
}.bind(this), 3000); //document
}
function fn(){
alert(this);
}
fn.call(div); // div
fn.apply(document); //document
fn.bind(inp)(); // input
</script> ES6相关内容let / constlet : 用于声明变量必须先声明,后使用。(变量不再做提升了)let声明的全局变量不是window对象的属性。在同一个作用域中,let不能重复声明同一个变量。let声明会产生块级作用域,for循环有两个作用域,for本身是一个作用域,for循环体又是子级作用域。const : 用于声明常量常量一旦声明,不允许修改。基本类型的数据,值不允许修改。复合类型的数据,引用地址不允许修改。let 和 const 的区别let 声明的变量的值可以改变,const 声明的变量的值不可以改变let 声明的时候可以不赋值,const 声明的时候必须赋值箭头函数箭头函数是 ES6 里面一个简写函数的语法方式重点: 箭头函数只能简写函数表达式,不能简写声明式函数function fn() {} // 不能简写
const fun = function () {} // 可以简写
const obj = {
fn: function () {} // 可以简写
}语法: (函数的行参) => { 函数体内要执行的代码 }const fn = function (a, b) {
console.log(a)
console.log(b)
}
// 可以使用箭头函数写成
const fun = (a, b) => {
console.log(a)
console.log(b)
}箭头函数不利于阅读箭头函数中没有this指向,指向上下文中的this.箭头函数不能实现构造函数箭头函数不能new建议在回调函数中使用箭头函数。箭头函数内部没有 arguments 这个参数集合函数的行参只有一个的时候可以不写 () 其余情况必须写函数体只有一行代码的时候,可以不写 {} ,并且会自动 return回调函数:当一个函数作为另一个函数的参数时,这个函数就是回调函数。函数参数默认值我们在定义函数的时候,有的时候需要一个默认值出现就是当我不传递参数的时候,使用默认值,传递参数了就使用传递的参数function fn(a) {
a = a || 10
console.log(a)
}
fn() // 不传递参数的时候,函数内部的 a 就是 10
fn(20) // 传递了参数 20 的时候,函数内部的 a 就是 20在 ES6 中我们可以直接把默认值写在函数的行参位置function fn(a = 10) {
console.log(a)
}
fn() // 不传递参数的时候,函数内部的 a 就是 10
fn(20) // 传递了参数 20 的时候,函数内部的 a 就是 20这个默认值的方式箭头函数也可以使用const fn = (a = 10) => {
console.log(a)
}
fn() // 不传递参数的时候,函数内部的 a 就是 10
fn(20) // 传递了参数 20 的时候,函数内部的 a 就是 20注意: 箭头函数如果你需要使用默认值的话,那么一个参数的时候也需要写 ()模板字符串ES5 中我们表示字符串的时候使用 '' 或者 ""在 ES6 中,我们还有一个东西可以表示字符串,就是 ``(反引号)let str = `hello world`
console.log(typeof str) // stringX和单引号还有双引号的区别反引号可以换行书写// 这个单引号或者双引号不能换行,换行就会报错了
let str = 'hello world'
// 下面这个就报错了
let str2 = 'hello
world'
let str = `
hello
world
`
console.log(str) // 是可以使用的反引号可以直接在字符串里面拼接变量// ES5 需要字符串拼接变量的时候
let num = 100
let str = 'hello' + num + 'world' + num
console.log(str) // hello100world100
// 直接写在字符串里面不好使
let str2 = 'hellonumworldnum'
console.log(str2) // hellonumworldnum
// 模版字符串拼接变量
let num = 100
let str = `hello${num}world${num}`
console.log(str) // hello100world100解构赋值对象解构:可以快速读取对象中的属性或方法。let {a = 1,b = 2,c = 3} = {c : 8,a : 2,b : 3}数组解构: 可以快速读取数组中的元素。let [a = 1,b = 2,c = 3] = [4,5,6];好处?可以快速交换两个变量中的值函数中的形参可以设置默认值函数中的形参可以不按照顺序传递函数中的返回值可以一次返回多个数据了。... 运算符(展开运算符)ES6 里面号新添加了一个运算符 ... ,叫做展开运算符作用是把数组展开let arr = [1, 2, 3, 4, 5]
console.log(...arr) // 1 2 3 4 5合并数组的时候可以使用let arr = [1, 2, 3, 4]
let arr2 = [...arr, 5]
console.log(arr2)也可以合并对象使用let obj = {
name: 'Jack',
age: 18
}
let obj2 = {
...obj,
gender: '男'
}
console.log(obj2)在函数传递参数的时候也可以使用let arr = [1, 2, 3]
function fn(a, b, c) {
console.log(a)
console.log(b)
console.log(c)
}
fn(...arr)
// 等价于 fn(1, 2, 3)对象简写形式当对象中的key和value的名字相同时,可以只写一个key.let id = 1;
let name = '手机';
let price = 4999;
//创建一个对象
let obj = {
id, // id : id 名字相同,可以简写
name,
price,
num : 2
}模块化语法 import / exportexport : 导出模块import : 导入模块实现方法先定义模块,再导出模块//定义模块
let user = '张三';
let age = 18;
function show(){
return '姓名' + user + '年龄' + age;
}
//导出模块
export {user,age,show};
//导入模块
import {user,age,show} from './tools.js';2. 边定义模块,边导出模块//边定义模块,边导出模块
export let user = '张三';
export let age = 18;
export function show(){
return `姓名${user},年龄${age}`;
}
//导入模块
import {user,age,show} from './tools.js';3.以别名的方式导出模块let a = '李四';
let b = 19;
function c(){
return `姓名:${a},年龄:${b}`;
}
export {a as user,b as age,c as show};
//导入模块
import {user,age,show} from './tools.js';4.导入 导出默认模块//导出默认模块(只能有一个)
let user = '王五';
let age = 20;
function a(){
return `姓名:${user},年龄:${age}`;
}
export {user,age};
export default a;
//导入模块
import {user,age} from './tools.js';
//导入默认模块
import display from './tools.js';Set / Map / for ... ofSet : 天然具有去重的功能。创建set对象let set = new Set(); let set = new Set([1,2,1,2,1,2,3]);2. 属性size : 长度3. 方法:set.add(元素) : 添加元素,返回set对象 set.has(元素) : 检测元素是否在set对象中,返回布尔值 set.delete(元素) : 删除指定元素,返回布尔值 set.clear() : 清空set对象 set.forEach((value,key,set) =>{}) : 遍历set对象 set.keys() : 获取所有的key set.values() : 获取所有的value set.entries() : 获取所有的key和value for offor(循环变量 of set|map){
语句组;
}Map创建map对象let map = new Map(); let map = new Map([ [1,'one'], [2,'two'], ['2','three'], [true,'four'], [3,'five'] ]);2. 属性size : 长度3. 方法:map.set('key','value') : 添加元素,返回map对象 map.get('key') : 获取value map.has(元素) : 检测元素是否在map对象中,返回布尔值 map.delete(元素) : 删除指定元素,返回布尔值 map.clear() : 清空map对象 map.forEach((value,key,map) =>{}) : 遍历map对象 map.keys() : 获取所有的key map.values() : 获取所有的value map.entries() : 获取所有的key和value for offor(循环变量 of set|map){
语句组;
}
Ned
少走些three.js的弯路
three.js使用注意事项1. 贴图反向texture.flipY = false;2. 贴图没有填充满模型textureMap.wrapS = textureMap.wrapT = THREE.RepeatWrapping;3. 贴图透明度transparent: false;
//树叶
blending: THREE.MultiplyBlending;4. 深度冲突无需深度检测的Material设置 depthTest:false
new THREE.WebGLRenderer( { logarithmicDepthBuffer: true } );5. 渲染顺序问题WebGLRenderer设置sortObjects: false;
每个Mesh手动设置renderOrder的顺序;6. 多层次细节const lod = new THREE.LOD();
//Create spheres with 3 levels of detail and create new LOD levels for them
for (let i = 0; i < 3; i++) {
const geometry = new THREE.IcosahedronBufferGeometry(10, 3 - i);
const mesh = new THREE.Mesh(geometry, material);
lod.addLevel(mesh, i * 75);
//addLevel ( object : Object3D, distance : Float ) : this
//object —— 在这个层次中将要显示的Object3D。
//distance —— 将显示这一细节层次的距离。
}
scene.add(lod);7. 抗锯齿//antialias - 是否执行抗锯齿。默认为false.
new THREE.WebGLRenderer({ antialias: true });8. 阴影//渲染器开启渲染阴影效果
renderer.shadowMapEnabled = true;
this.renderer.shadowMap.enable = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
//平面接收投影
plane.receiveShadow = true;
//点光源产生投影
spotLight.castShadow = true;
//物体对象产生投影
cube.castShadow = true;阴影使用可能遇到的问题● 阴影模糊,增加 shadowMapWidth 和 shadowMapHeight,或保证用于计算阴影区域紧密包围在对象周围(shadowCameraNear, shadowCameraFar, shadowCameraFov)● 产生阴影与接收阴影设置,光源生成阴影,几何体是否接收或投射阴影 castShadow 和 receiveShadow● 薄对象渲染阴影时可能出现奇怪的渲染失真,可通过 shadowBias 轻微偏移阴影来修复● 调整 shadowDarkness 来改变阴影的暗度● 阴影更柔和,可在 THREE. WebGLRenderer 设置不同 shadowMapType。默认 THREE. PCFShadowMap, 柔和:PCFSoftShadowMap9. html 标签,CSS2DRendererconst moonDiv = document.createElement('div');
moonDiv.innerHTML = 'Moon';
//保证能点击
moonDiv.style.pointerEvents = 'auto';
const moonLabel = new CSS2DObject(moonDiv);
moonLabel.position.set(0, 10, 0);
moon.add(moonLabel);
labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(container.offsetWidth, container.offsetHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
//不妨碍界面上的东东
labelRenderer.domElement.style.pointerEvents = 'none';
container.appendChild(labelRenderer.domElement);
function onWindowResize() {
labelRenderer.setSize(container.offsetWidth, container.offsetHeight);
}
function animate() {
labelRenderer.render(scene, camera);
}10. 颜色问题//底色透明
this.renderer.setClearColor(0x000000, 0);
//模型渲染,默认THREE.LinearEncoding
this.renderer.outputEncoding = THREE.sRGBEncoding;
THREE.LinearEncoding;
THREE.sRGBEncoding;
THREE.GammaEncoding;
THREE.RGBEEncoding;
THREE.LogLuvEncoding;
THREE.RGBM7Encoding;
THREE.RGBM16Encoding;
THREE.RGBDEncoding;
THREE.BasicDepthPacking;
THREE.RGBADepthPacking;11. 分辨率问题this.renderer.setPixelRatio(window.devicePixelRatio);
//分辨率越高渲染压力就越大12. 物体居中function setModelCenter(object, viewControl) {
if (!object) {
return;
}
if (object.updateMatrixWorld) {
object.updateMatrixWorld();
}
// 获得包围盒得min和max
const box = new THREE.Box3().setFromObject(object);
let objSize = box.getSize(new THREE.Vector3());
// 返回包围盒的中心点
const center = box.getCenter(new THREE.Vector3());
object.position.x += object.position.x - center.x;
object.position.y += object.position.y - center.y;
object.position.z += object.position.z - center.z;
let width = objSize.x;
let height = objSize.y;
let depth = objSize.z;
let centroid = new THREE.Vector3().copy(objSize);
centroid.multiplyScalar(0.5);
if (viewControl.autoCamera) {
this.camera.position.x =
centroid.x * (viewControl.centerX || 0) + width * (viewControl.width || 0);
this.camera.position.y =
centroid.y * (viewControl.centerY || 0) + height * (viewControl.height || 0);
this.camera.position.z =
centroid.z * (viewControl.centerZ || 0) + depth * (viewControl.depth || 0);
} else {
this.camera.position.set(
viewControl.cameraPosX || 0,
viewControl.cameraPosY || 0,
viewControl.cameraPosZ || 0
);
}
this.camera.lookAt(0, 0, 0);
}13. 清空资源function cleanNext(obj, idx) {
if (idx < obj.children.length) {
this.cleanElmt(obj.children[idx]);
}
if (idx + 1 < obj.children.length) {
this.cleanNext(obj, idx + 1);
}
}
function cleanElmt(obj) {
if (obj) {
if (obj.children && obj.children.length > 0) {
this.cleanNext(obj, 0);
obj.remove(...obj.children);
}
if (obj.geometry) {
obj.geometry.dispose && obj.geometry.dispose();
}
if (obj.material) {
for (const v of Object.values(obj.material)) {
if (v instanceof THREE.Texture) {
v.dispose && v.dispose();
}
}
obj.material.dispose && obj.material.dispose();
}
obj.dispose && obj.dispose();
obj.clear && obj.clear();
}
}
function cleanObj(obj) {
this.cleanElmt(obj);
obj?.parent?.remove && obj.parent.remove(obj);
}
function cleanAll() {
window.removeEventListener('resize');
cancelAnimationFrame(this.threeAnim);
if (this.stats) {
this.container.removeChild(this.stats.domElement);
this.stats = null;
}
this.cleanObj(this.scene);
this.controls && this.controls.dispose();
this.renderer.renderLists && this.renderer.renderLists.dispose();
this.renderer.dispose && this.renderer.dispose();
this.renderer.forceContextLoss();
let gl = this.renderer.domElement.getContext('webgl');
gl && gl.getExtension('WEBGL_lose_context').loseContext();
this.renderer.setAnimationLoop(null);
this.renderer.domElement = null;
this.renderer.content = null;
console.log('清空资源', this.renderer.info);
this.renderer = null;
THREE.Cache.clear();
if (this.map) {
this.map.destroy();
}
}14. 模型显示面的问题.side:Integer定义将要渲染哪一面 - 正面,背面或两者。 默认为 THREE.FrontSide。其他选项有 THREE.BackSide 和 THREE.DoubleSide。material.side = THREE.DoubleSide;15. Raycaster 鼠标拾取不要检测全局,用 actionObjs 收集需要动作的物体this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.container.style.cursor = 'pointer';
this.container.addEventListener(
'pointerdown',
(event) => {
event.preventDefault();
this.mouse.x =
((event.offsetX - this.container.offsetLeft) / this.container.offsetWidth) * 2 - 1;
this.mouse.y =
-((event.offsetY - this.container.offsetTop) / this.container.offsetHeight) * 2 + 1;
let vector = new THREE.Vector3(this.mouse.x, this.mouse.y, 1).unproject(this.camera);
this.raycaster.set(this.camera.position, vector.sub(this.camera.position).normalize());
this.raycaster.setFromCamera(this.mouse, this.camera);
const intersects = raycaster.intersectObjects(this.actionObjs, true);
if (intersects?.length) {
console.log('action', intersects[0]);
this.raycasterAction(intersects[0]);
}
},
false
);16. Canvas 贴图生成的 canvas 大小最好是正常模型贴图大小的五倍以上,有可能因为缩放问题,导致贴图模糊17.打包后线上效果与开发时效果存在差异将three的相关js提到html上,从外部引入,这样能保证three不会因为打包而乱了,导致效果有问题 FBXLoader外部引入,记得把libs里面的inflate也加上18.THREE.js截图new THREE.WebGLRenderer({
preserveDrawingBuffer: true //保留缓冲区
});
import { saveAs } from 'file-saver-fixed';
function convertBase64UrlToBlob(base64) {
let parts = base64.split(';base64,');
let contentType = parts[0].split(':')[1];
let raw = window.atob(parts[1]);
let rawLength = raw.length;
let uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i < rawLength; i++) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], { type: contentType });
}
saveImage: () => {
let image = threeModel.renderer.domElement.toDataURL('image/jpeg');
let blob = convertBase64UrlToBlob(image);
saveAs(blob, new Date().getTime() + '.jpg');
}18.glb压缩过的模型加载,记得加DRACOLoader记得将three.js/examples/js/libs/draco/gltf目录下的draco解码器全部放在public/draco文件夹下,否则会导致模型加载失败!let dracoLoader = new THREE.DRACOLoader();
dracoLoader.setDecoderPath('draco/');
dracoLoader.setDecoderConfig({ type: 'js' });//或者{type: "wasm"}
dracoLoader.preload();
const loader = new THREE.GLTFLoader();
loader.setDRACOLoader(dracoLoader);
return loader;
19.大量复用模型可采用InstancedMesh减少绘制程序调用的次数,提升渲染效率//count 需要生成的相同模型数量
let mesh = new THREE.InstancedMesh(geometry, material, count);
//动态生成,`THREE.DynamicDrawUsage` mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
//设置第index个模型的位置
mesh.setMatrixAt(index, matrix);
//位置更新
mesh.instanceMatrix.needsUpdate = true;
//设置第index个模型的颜色
mesh.setColorAt(index,new THREE.Color(1,0,0));
//颜色更新
mesh.instanceColor.needsUpdate = true;
20.合并模型BufferGeometry包含点线面等相关的缓冲区数据,使用它能降低将所有这些数据传递到GPU的成本将多个形状合并成一个,减少模型数量,提升渲染效率!const geometries = [];
const geometry = new THREE.BufferGeometry().fromGeometry(new THREE.BoxGeometry( 10, 10, 10 ));
geometry.applyMatrix4( matrix );//模型位置
geometries.push( geometry );//将模型缓冲几何形状添加到数组
//...
const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries( geometries );合并后的模型就属于一个整体,动作检测时只能检测到这个整体,不能检测子模型如果需要监听到merge后的具体模型,需要做一些处理在每个geometry添加到数组时,也要设置模型顺序索引modelIndex和选中索引selectIndex,并且收集每个geometry的面数,如果是同一个形状的话,面数直接用同一个就好,这样计算更方便。const count = geometry.getAttribute('position').count;
const modelIndex = new Uint8Array(count);
const selectIndex = new Uint8Array(count);
for (let i = 0; i < count; i++) {
modelIndex[i] = index;
selectIndex[i] = -1;
}
geometry.setAttribute('selectIndex', new THREE.BufferAttribute(selectIndex, 1, true));
geometry.setAttribute('modelIndex', new THREE.BufferAttribute(modelIndex, 1, true));2.而赋值给merge后形状的Mesh的材质也要对应修改,给顶点着色器和片元着色器添加代码,判断当前模型索引是否等于选中的索引,然后赋予需要的颜色。 const material = new THREE.MeshBasicMaterial({
vertexColors: true
});
material.onBeforeCompile = (shader) => {
shader.vertexShader = shader.vertexShader.replace(
`void main() {`,
` attribute float selectIndex;
attribute float modelIndex;
varying float vselectIndex;
varying float vmodelIndex;
void main() {
vmodelIndex=modelIndex;
vselectIndex=selectIndex;`
);
shader.fragmentShader = shader.fragmentShader.replace(
`void main() {`,
`varying float vselectIndex;
varying float vmodelIndex;
void main() {`
);
shader.fragmentShader = shader.fragmentShader.replace(
`vec3 outgoingLight = reflectedLight.indirectDiffuse;`,
`vec3 outgoingLight = vmodelIndex ==vselectIndex ?vec3(1.0,0.0,0.0 ): reflectedLight.indirectDiffuse ;`
);
};3. Raycaster点击后的获得的信息中有个叫做fanceIndex的属性,就是这个形状中某个面的索引,然后根据面索引和面数算出具体选中的模型索引selectIndex const intersects = this.raycaster.intersectObjects(this.actionGroup, true);
if (intersects.length > 0) {
let activeObj = intersects[0];
let index = parseInt(activeObj.faceIndex / 12);//正方体有12个面
let len = this.boxmesh.geometry.getAttribute('position').count;
const selectIndex = new Uint8Array(len);
for (let i = 0; i < len; i++) {
selectIndex[i] = index;
}
this.boxmesh.geometry.setAttribute(
'selectIndex',
new THREE.BufferAttribute(selectIndex, 1, true)
);
console.log(activeObj, this.boxmesh.geometry, index);
this.boxmesh.geometry.getAttribute('selectIndex').needsUpdate = true;//一定要记得更新选中索引值
}
然后如图所见,就能选中某个形状啦! github地址:https://github.com/xiaolidan00/my-earth/blob/main/src/mergeGeometry.html后期处理部分辉光官网给出的是Layer分层的方案,感觉操作起来很烦,我推荐的是直接用visible控制要渲染出泛光效果的组件和不需要渲染泛光效果组件,然后将这两组渲染结果合并就是最终的画面了 this.renderer.setViewport(0, 0, this.container.offsetWidth, this.container.offsetHeight);
//必须关闭autoClear,避免渲染效果被清除
this.renderer.autoClear = false;
this.renderer.clear();
//不需要发光的物体在bloom后期前隐藏
this.normalObj.visible = false;
//渲染泛光的场景
this.composer.render();
//清除深度缓存
this.renderer.clearDepth();
//不需要发光的物体在bloom后期后显示
this.normalObj.visible = true;
//合并两个渲染场景,即可部分泛光
this.renderer.render(this.scene, this.camera);
github地址:https://github.com/xiaolidan00/my-earth/blob/main/src/UnrealBloom.html后续开发遇到问题会继续更新~
Ned
JavaScript面向对象编程的奥秘揭秘:掌握核心概念与设计模式
什么是面向对象?把数据及对数据的操作方法放在一起,作为一个相互依存的整体——对象。对同类对象抽象出其共性,形成类。类中的大多数数据,只能用本类的方法进行处理。首先,我们要明确,面向对象不是语法,是一个思想,是一种 编程模式面向: 面(脸),向(朝着)面向过程: 脸朝着过程 =》 关注着过程的编程模式面向对象: 脸朝着对象 =》 关注着对象的编程模式实现一个效果在面向过程的时候,我们要关注每一个元素,每一个元素之间的关系,顺序,。。。在面向对象的时候,我们要关注的就是找到一个对象来帮我做这个事情,我等待结果例子 🌰: 我要吃面条面向过程用多少面粉用多少水怎么和面怎么切面条做开水煮面吃面面向对象找到一个面馆叫一碗面等着吃面向对象就是对面向过程的封装我们以前的编程思想是,每一个功能,都按照需求一步一步的逐步完成我们以后的编程思想是,每一个功能,都先创造一个 面馆,这个 面馆 能帮我们作出一个 面(完成这个功能的对象),然后用 面馆 创造出一个 面,我们只要等到结果就好了类与对象的主要区别对象:对象是类的一个实例(对象不是找个女朋友),有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。类:类是一个模板,它描述一类对象的行为和状态。创建对象的方式因为面向对象就是一个找到对象的过程所以我们先要了解如何创建一个对象调用系统内置的构造函数创建对象js 给我们内置了一个 Object 构造函数这个构造函数就是用来创造对象的当 构造函数 和 new 关键字连用的时候,就可以为我们创造出一个对象因为 js 是一个动态的语言,那么我们就可以动态的向对象中添加成员了// 就能得到一个空对象
var o1 = new Object()
// 正常操作对象
o1.name = 'Jack'
o1.age = 18
o1.gender = '男'字面量的方式创建一个对象直接使用字面量的形式,也就是直接写 {}可以在写的时候就添加好成员,也可以动态的添加// 字面量方式创建对象
var o1 = {
name: 'Jack',
age: 18,
gender: '男'
}
// 再来一个
var o2 = {}
o2.name = 'Rose'
o2.age = 20
o2.gender = '女'使用工厂函数的方式创建对象先书写一个工厂函数这个工厂函数里面可以创造出一个对象,并且给对象添加一些属性,还能把对象返回使用这个工厂函数创造对象// 1. 先创建一个工厂函数
function createObj() {
// 手动创建一个对象
var obj = new Object()
// 手动的向对象中添加成员
obj.name = 'Jack'
obj.age = 18
obj.gender = '男'
// 手动返回一个对象
return obj
}
// 2. 使用这个工厂函数创建对象
var o1 = createObj()
var o2 = createObj()使用自定义构造函数创建对象工厂函数需要经历三个步骤手动创建对象手动添加成员手动返回对象构造函数会比工厂函数简单一下自动创建对象手动添加成员自动返回对象先书写一个构造函数在构造函数内向对象添加一些成员使用这个构造函数创造一个对象(和 new 连用)构造函数可以创建对象,并且创建一个带有属性和方法的对象面向对象就是要想办法找到一个有属性和方法的对象面向对象就是我们自己制造 构造函数 的过程// 1. 先创造一个构造函数
function Person(name, gender) {
this.age = 18
this.name = name
this.gender = gender
}
// 2. 使用构造函数创建对象
var p1 = new Person('Jack', 'man')
var p2 = new Person('Rose', 'woman')构造函数详解我们了解了对象的创建方式我们的面向对象就是要么能直接得到一个对象要么就弄出一个能创造对象的东西,我们自己创造对象我们的构造函数就能创造对象,所以接下来我们就详细聊聊 构造函数构造函数的基本使用和普通函数一样,只不过 调用的时候要和 new 连用,不然就是一个普通函数调用T4Zfunction Person() {}
var o1 = new Person() // 能得到一个空对象
var o2 = Person() // 什么也得不到,这个就是普通函数调用注意: 不写 new 的时候就是普通函数调用,没有创造对象的能力首字母大写function person() {}
var o1 = new person() // 能得到一个对象
function Person() {}
var o2 = new Person() // 能得到一个对象注意: 首字母不大写,只要和 new 连用,就有创造对象的能力当调用的时候如果不需要传递参数可以不写 (),建议都写上function Person() {}
var o1 = new Person() // 能得到一个空对象
var o2 = new Person // 能得到一个空对象 注意: 如果不需要传递参数,那么可以不写 (),如果传递参数就必须写构造函数内部的 this,由于和 new 连用的关系,是指向当前实例对象的function Person() {
console.log(this)
}
var o1 = new Person() // 本次调用的时候,this => o1
var o2 = new Person() // 本次调用的时候,this => o2注意: 每次 new 的时候,函数内部的 this 都是指向当前这次的实例化对象因为构造函数会自动返回一个对象,所以构造函数内部不要写 return你如果 return 一个基本数据类型,那么写了没有意义如果你 return 一个引用数据类型,那么构造函数本身的意义就没有了使用构造函数创建一个对象我们在使用构造函数的时候,可以通过一些代码和内容来向当前的对象中添加一些内容function Person() {
this.name = 'Jack'
this.age = 18
}
var o1 = new Person()
var o2 = new Person()我们得到的两个对象里面都有自己的成员 name 和 age我们在写构造函数的时候,是不是也可以添加一些方法进去呢?function Person() {
this.name = 'Jack'
this.age = 18
this.sayHi = function () {
console.log('hello constructor')
}
}
var o1 = new Person()
var o2 = new Person()显然是可以的,我们的到的两个对象中都有 sayHi 这个函数也都可以正常调用但是这样好不好呢?缺点在哪里?function Person() {
this.name = 'Jack'
this.age = 18
this.sayHi = function () {
console.log('hello constructor')
}
}
// 第一次 new 的时候, Person 这个函数要执行一遍
// 执行一遍就会创造一个新的函数,并且把函数地址赋值给 this.sayHi
var o1 = new Person()
// 第二次 new 的时候, Person 这个函数要执行一遍
// 执行一遍就会创造一个新的函数,并且把函数地址赋值给 this.sayHi
var o2 = new Person()这样的话,那么我们两个对象内的 sayHi 函数就是一个代码一摸一样,功能一摸一样但是是两个空间函数,占用两个内存空间也就是说 o1.sayHi 是一个地址,o2.sayHi 是一个地址所以我们执行 console.log(o1 === o2.sayHi) 的到的结果是 false缺点: 一摸一样的函数出现了两次,占用了两个空间地址怎么解决这个问题呢?就需要用到一个东西,叫做 原型原型原型的出现,就是为了解决 构造函数的缺点也就是给我们提供了一个给对象添加函数的方法不然构造函数只能给对象添加属性,不能合理的添加函数就太 LOW 了prototype每一个函数天生自带一个成员,叫做 prototype,是一个对象空间即然每一个函数都有,构造函数也是函数,构造函数也有这个对象空间这个 prototype 对象空间可以由函数名来访问function Person() {} console.log(Person.prototype) // 是一个对象即然是个对象,那么我们就可以向里面放入一些东西function Person() {} Person.prototype.name = 'prototype' Person.prototype.sayHi = function () {}我们发现了一个叫做 prototype 的空间是和函数有关联的并且可以向里面存储一些东西重点: 在函数的 prototype 里面存储的内容,不是给函数使用的,是给函数的每一个实例化对象使用的那实例化对象怎么使用能?__proto__每一个对象都天生自带一个成员,叫做 __proto__,是一个对象空间即然每一个对象都有,实例化对象也是对象,那么每一个实例化对象也有这个成员这个 __proto__ 对象空间是给每一个对象使用的当你访问一个对象中的成员的时候如果这个对象自己本身有这个成员,那么就会直接给你结果如果没有,就会去 __proto__ 这个对象空间里面找,里面有的话就给你结果未完待续。。。那么这个 __proto__ 又指向哪里呢?这个对象是由哪个构造函数 new 出来的那么这个对象的 __proto__ 就指向这个构造函数的 prototypefunction Person() {}
var p1 = new Person()
console.log(p1.__proto__ === Person.prototype) // true我们发现实例化对象的 __proto__ 和所属的构造函数的 prototype 是一个对象空间我们可以通过构造函数名称来向 prototype 中添加成员对象在访问的时候自己没有,可以自动去自己的 __proto__ 中查找那么,我们之前构造函数的缺点就可以解决了我们可以把函数放在构造函数的 prototype 中实例化对象访问的时候,自己没有,就会自动去 __proto__ 中找那么也可以使用了function Person() {}
Person.prototype.sayHi = function () {
console.log('hello Person')
}
var p1 = new Person()
p1.sayHi()p1 自己没有 sayHi 方法,就会去自己的 __proto__ 中查找p1.__proto__ 就是 Person.prototype我们又向 Person.prototype 中添加了 sayHi 方法所以 p1.sayHi 就可以执行了到这里,当我们实例化多个对象的时候,每个对象里面都没有方法都是去所属的构造函数的 protottype 中查找那么每一个对象使用的函数,其实都是同一个函数那么就解决了我们构造函数的缺点function Person() {}
Person.prototype.sayHi = function () {
console.log('hello')
}
var p1 = new Person()
var p2 = new Person()
console.log(p1.sayHi === p2.sayHi)p1 是 Person 的一个实例p2 是 Person 的一个实例也就是说 p1.__proto__ 和 p2.__proto__ 指向的都是 Person.prototype当 p1 去调用 sayHi 方法的时候是去 Person.prototype 中找当 p2 去调用 sayHi 方法的时候是去 Person.prototype 中找那么两个实例化对象就是找到的一个方法,也是执行的一个方法结论当我们写构造函数的时候属性我们直接写在构造函数体内方法我们写在原型上原型链我们刚才聊过构造函数了,也聊了原型那么问题出现了,我们说构造函数的 prototype 是一个对象又说了每一个对象都天生自带一个 __proto__ 属性那么 构造函数的 prototype 里面的 __proto__ 属性又指向哪里呢?一个对象所属的构造函数每一个对象都有一个自己所属的构造函数比如: 数组
// 数组本身也是一个对象
var arr = []
var arr2 = new Array()
以上两种方式都是创造一个数组我们就说数组所属的构造函数就是 Array比如: 函数// 函数本身也是一个对象
var fn = function () {}
var fun = new Function()以上两种方式都是创造一个函数我们就说函数所属的构造函数就是 Functionconstructor对象的 __proto__ 里面也有一个成员叫做 constructor这个属性就是指向当前这个对象所属的构造函数链状结构当一个对象我们不知道准确的是谁构造的时候,我们呢就把它看成 Object 的实例化对象也就是说,我们的 构造函数 的 prototype 的 __proto__ 指向的是 Object.prototype那么 Object.prototype 也是个对象,那么它的 __proto__ 又指向谁呢?因为 Object 的 js 中的顶级构造函数,我们有一句话叫 万物皆对象所以 Object.prototype 就到顶了,Object.prototype 的 __proto__ 就是 null原型链的访问原则我们之前说过,访问一个对象的成员的时候,自己没有就会去 __proto__ 中找接下来就是,如果 __proto__ 里面没有就再去 __proto__ 里面找一直找到 Object.prototype 里面都没有,那么就会返回 undefiend对象的赋值到这里,我们就会觉得,如果是赋值的话,那么也会按照原型链的规则来但是: 并不是!并不是!并不是! 重要的事情说三遍赋值的时候,就是直接给对象自己本身赋值如果原先有就是修改原先没有就是添加不会和 __proto__ 有关系总结到了这里,我们就发现了面向对象的思想模式了当我想完成一个功能的时候先看看内置构造函数有没有能给我提供一个完成功能对象的能力如果没有,我们就自己写一个构造函数,能创造出一个完成功能的对象然后在用我们写的构造函数 new 一个对象出来,帮助我们完成功能就行了比如: tab选项卡我们要一个对象对象包含一个属性:是每一个点击的按钮对象包含一个属性:是每一个切换的盒子对象包含一个方法:是点击按钮能切换盒子的能力那么我们就需要自己写一个构造函数,要求 new 出来的对象有这些内容就好了然后在用构造函数 new 一个对象就行了面向对象改写案例 - 选项卡<div id="box">
<ul>
<li class="active">1</li>
<li>2</li>
<li>3</li>
</ul>
<ol>
<li class="active">1</li>
<li>2</li>
<li>3</li>
</ol>
</div>
*{
padding: 0;
margin: 0;
}
#box{
width: 500px;
height: 320px;
display: flex;
flex-direction: column;
margin: 50px auto;
border: 3px solid #333;
}
#box>ul{
height: 60px;
display: flex;
}
#box > ul > li {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
font-size: 30px;
color: #fff;
background-color: skyblue;
cursor: pointer;
}
#box > ul > li.active{
background-color: orange;
}
#box > ol {
flex: 1;
position: relative;
}
#box > ol > li {
width: 100%;
height: 100%;
background-color: purple;
font-size: 50px;
color: #fff;
position: absolute;
left: 0;
top: 0;
display: none;
}
#box > ol > li.active {
display: block;
}
//创建自定义构造函数
function Tab(){
//tab选项
this.tab = document.querySelectorAll('ul>li');
//内容选项
this.content = document.querySelectorAll('ol>li');
//添加事件
this.addEvent();
}
//添加原型方法
Tab.prototype.addEvent = function(){
//遍历添加事件
for(let i = 0,len = this.tab.length;i < len;i ++){
this.tab[i].onclick = function(){
//将所有的选项移除类名
for(let j = 0;j < this.tab.length;j ++){
this.tab[j].classList.remove('active');
this.content[j].classList.remove('active');
}
//给当前选项添加类名
this.tab[i].classList.add('active');
this.content[i].classList.add('active');
}.bind(this);
}
}
//创建对象
new Tab();
Ned
掌握JavaScript的数学与时间魔法:Math和Date对象详解
Math对象(Math对象不能new!!!!)Math.PI 圆周率Math.abs() 求绝对值Math.round() 四舍五入如果负数时, > 0.5 进一 <=0.5 舍去console.log(Math.round(4.5)); //5
console.log(Math.round(4.4)); //4
console.log(Math.round(-4.5)); // -4
console.log(Math.round(-4.5000001)); // -5
console.log(Math.round(-4.4)); //-4
console.log(Math.round(-4.6)); //-54. Math.ceil() 向上取整console.log(Math.ceil(4.1)); //5
console.log(Math.ceil(4.9)); //5
console.log(Math.ceil(-4.1)); //-4
console.log(Math.ceil(-4.9)); // -45. Math.max() 最大值求数组中最大值: Math.max.apply(null,数组) Math.max( ... 数组名)求数组中最小值: Math.min.apply(null,数组) Math.min( ... 数组名)var arr = [2,3,1,4,5,3,4];
console.log(Math.max(2,3,1,4,5,3,4));
console.log(Math.min(2,3,1,4,5,3,4));
console.log(Math.max.apply(null,arr));
console.log(Math.min.apply(null,arr));6. Math.pow(m,n) 求m的n次方7. Math.sqrt(n) 求n的开方8. Math.random() 随机数 0 <= n < 1Math.floor(Math.random() * (max - min + 1) + min) 推荐 Math.round(Math.random() * (max - min) + min)function random(min,max){
if(min > max){
var t = min;
min = max;
max = t;
}
return Math.floor(Math.random() * (max - min + 1) + min);
}案例实现猜数字游戏(1~10,三次机会)<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="button" value="你猜?" id="btn">
<script>
//实现猜数字游戏(1~10,三次机会)
/*
1~10 ? 得到一个什么样的信息? 随机数
三次机会?想到了什么呢? 用户输入三次,循环3次
如果第一次用户就猜中了?后面的两次机会还需要吗?如果不需要了?怎么办? break
*/
//一、获取页面元素
var o_btn = document.getElementById('btn');
//二、添加事件
o_btn.onclick = function(){
//1. 随机一个整数:(1~10)
var rand = Math.floor(Math.random() * 10 + 1);
//2. 让用户去猜,且有三次猜的机会,所以使用循环
for(var i = 1;i < 4;i ++){
//问用户接收一个整数
var n = parseInt(prompt('请输入1~10中的一个整数:'));
//检测用户输入的整数是否等于随机数
if(rand === n){
alert('您猜中了!');
break; //退出循环
}else if(n > rand){
alert('您猜大了!');
}else{
alert('您猜小了!');
}
}
//3. 三次机会用完了
if(i === 4){
alert('很遗憾,你挑战失败!正确的数字是:' + rand);
}
}
</script>
</body>
</html>2. 封装一个随机颜色的函数(至少封装三种)<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#box{
width: 100px;
height: 100px;
border: 1px solid black;
}
</style>
</head>
<body>
<input type="button" value="你猜?" id="btn">
<div id="box"></div>
<script>
//封装一个随机颜色的函数(至少封装三种)
/*
1. 单词 ['red','green','blue'] pass
2. #十六进制 6个 #ffffff 白色 #000000 黑色
3. rgb(0~255,0~255,0~255)
*/
function randColor01(){
// 在乘的时候会自动将(0xffffff)这个十六进制数转为十进制,求出随机数后再转为十六进制
return '#' + Math.floor(Math.random() * 0xffffff).toString(16);
}
function randColor02(){
var str = '#';
//颜色有6个十六进制数
for(var i = 0;i < 6;i ++){
str += Math.floor(Math.random() * 17).toString(16);
}
return str;
}
function randColor03(){
var arr = [];//放置颜色值
for(var i = 0;i < 3;i ++){
arr.push(Math.floor(Math.random() * 256)); //[45,23,233]
}
// 45,23,233
return 'rgb(' + arr.join() + ')';
}
function randColor04(){
var str = 'rgb(';//放置颜色值
for(var i = 0;i < 3;i ++){
str += Math.floor(Math.random() * 256) + ','; //'rgb(33,44,55,'
}
return str.slice(0,-1) + ')';
}
console.log(randColor01());
console.log(randColor02());
console.log(randColor03());
// alert(randColor03());
//获取按钮
var o_btn = document.getElementById('btn');
//获取div
var o_div = document.getElementById('box');
//添加事件
o_btn.onclick = function(){
//改变div的背景颜色
o_div.style.backgroundColor = randColor03();
}
</script>
</body>
</html>3. 封装一个4位验证码的函数(包含数字大写字母小写字母) <script>
/*
封装一个4位验证码的函数(包含数字大写字母小写字母)
4位验证码? 循环四次
包含数字大写字母小写字母? 数组 字符串 下标
*/
function verificationCode(n){
//包含的内容
var str = '0123456789qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPLKJHGFDSAZXCVBNM';
var code = '';
//n位验证码
for(var i = 0;i < n;i ++){
code += str.charAt(Math.floor(Math.random() * str.length));
}
return code;
}
alert(verificationCode(6));
</script>4. 扁平化数组 如:[1,2,[3,[4,5],[6,7],8],9] [1,2,3,4,5,6,7,8,9]<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
/*
扁平化数组 如:[1,2,[3,[4,5],[6,7],8],9] [1,2,3,4,5,6,7,8,9]
1. 不知道里面有多少层数组,也就不知道嵌套多少层循环,这时候,使用递归解决
instanceof : 检测当前对象是否属于某一个类返回布尔
*/
// [1,[2,3]] [1,2,3]
function delayering(data){ //data : 数据
//准备一个新数组
//1.2.1
var list = [];
//1. 判断data 是否为数组
if(data instanceof Array){
//如果data是数组,返回true
//遍历数组
//1.1
for(var i = 0,len = data.length;i < len;i ++){
list = list.concat(delayering(data[i]));
}
}else{ //如果data不是数组,返回false,执行else
//不是数组的这个元素,给它添加到一个新数组中
//1.2
list.push(data);
}
//返回新数组
//2
return list;
}
console.log(delayering([1,[2,3]]));
console.log(delayering([1,2,[3,[4,5],[6,7],8],9]))
data = [1,[2,3]]
var list = [1,2,3];
list = list.concat(delayering(1)); [1]
list = list.concat(delayering([2,3])); [2,3]
return list; [1,2,3]
data = 1
var list = []
list.push(1); [1]
return [1]
data = [2,3]
var list = [2,3];
list = list.concat(delayering(2)); [2]
list = list.concat(delayering(3)); [3]
return [2,3]
data = 2
var list = [];
list.push(2); [2]
return [2];
data = 3
var list = [];
list.push(3); [3]
return [3];
</script>
</body>
</html>计时器timer = setInterval(函数,毫秒数) : 间歇性计时器clearInterval(timer) : 取消计时器2. timer = setTimeout(函数,毫秒数) : 一次性计时器、延时器、定时器clearTimeout(timer);Date对象如何创建日期对象?var date = new Date();var date = new Date(2002,2,5); //0~11var date = new Date(2002,2,5,18,30,50); //0~11var date = new Date('2002-3-5'); //1~12var date = new Date('2002-3-5 18:30:50'); //1~12var date = new Date('2002/3/5'); //1~12var date = new Date('2002/3/5 18:30:50'); //1~12如何访问日期对象中的信息?date.getFullYear() //年date.getMonth() //月 0~11date.getDate() //日date.getDay() //星期date.getHours() 时date.getMinutes() 分date.getSeconds() 秒date.getMilliseconds() 毫秒date.getTime() 时间戳如何设置日期对象中的信息?date.setFullYear() 年date.setMonth() 0~11date.setDate() 日date.setHours() 时date.setMinutes() 分date.setSeconds() 秒date.setMilliseconds() 毫秒date.setTime() 时间戳如何以本地格式的字符串输出日期对象?date.toLocaleString() 本地格式的日期时间字符串date.toLocaleDateString() 本地格式的日期字符串date.toLocaleTimeString() 本地格式的时间字符串案例求出自己已生活了多少天零多少小时零多少分钟零多少秒钟? <script>
//求出自己已生活了多少天零多少小时零多少分钟零多少秒钟?
// 1天 = 24小时
// 1小时 = 60分
// 1分 = 60秒
// 1秒 = 1000毫秒
//1. 获取当前的日期时间
var now = new Date();
//2. 生日
var birthday = new Date('1984/3/5 18:30:50');
//3. 求两个时间的差值
// var minus = now - birthday; //毫秒数
var minus = Math.floor((now.getTime() - birthday.getTime()) / 1000); //秒
//4. 求天数
var date = Math.floor(minus / 60 / 60 / 24);
//5. 求小时
var hours = Math.floor((minus - date * 24 * 60 * 60) / 60 / 60);
//6. 分钟
var minutes = Math.floor((minus - date * 24 * 60 * 60 - hours * 60 * 60) / 60);
//7. 秒
var seconds = minus % 60;
console.log(date + '天' + hours + '时' + minutes + '分' + seconds + '秒');
</script>2. 写出距当前日期7天后的日期时间(注,使用日期对象的方法实现,不允许自己计算) <script>
//写出距当前日期7天后的日期时间(注,使用日期对象的方法实现,不允许自己计算)
// date date.setDate(date.getDate() + 7)
function nDaysAfter(n){
var date = new Date(); //创建日期对象
//设置当前的日 ( 当前的日期 + 指定的天数)
date.setDate(date.getDate() + n);
return date;
}
console.log(nDaysAfter(3).toLocaleString());
console.log(nDaysAfter(7).toLocaleString());
</script>3. 数码时钟<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.sp{
font-size: 100px;
color: red;
}
</style>
</head>
<body>
<div id="time">
<img src="img/0.jpg" alt="">
<img src="img/0.jpg" alt="">
<span class="sp">:</span>
<img src="img/0.jpg" alt="">
<img src="img/0.jpg" alt="">
<span class="sp">:</span>
<img src="img/0.jpg" alt="">
<img src="img/0.jpg" alt="">
</div>
<script>
/*
document.querySelectorAll(选择器) : 通过选择器获取所有的元素。返回一个伪数组(类数组)
伪数组(类数组):
1. 与数组相同的地方:都可以通过下标访问。都有length属性
2. 与数组不同的地方:伪数组没有数组的方法。
*/
//一、获取页面元素
//获取div#time中所有的img
var o_img = document.querySelectorAll('#time>img');
setInterval(function(){
//二、获取当前本地时间
//1. 创建日期对象
var date = new Date();
//1.1 获取时
var hours = date.getHours();
//1.2 获取分
var minutes = date.getMinutes();
//1.3 获取秒
var seconds = date.getSeconds();
//2. 将时分秒三段数字分解为6个数字并存在一个数组中
// 11 2
var arr = [
hours < 10 ? 0 : Math.floor(hours / 10), hours % 10,
minutes < 10 ? 0 : Math.floor(minutes / 10),minutes % 10,
seconds < 10 ? 0 : Math.floor(seconds / 10),seconds % 10
]
console.log(arr);
//3. 将图片中的src地址更改为当前时间对象的数字
//遍历伪数组
for(var i = 0,len = o_img.length;i < len;i ++){
o_img[i].src = 'img/' + arr[i] + '.jpg';
}
},1000)
</script>
</body>
</html>4. 倒计时<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text" name="" id="hours">:
<input type="text" name="" id="minutes">:
<input type="text" name="" id="seconds">
<script>
//获取元素
var o_hours = document.getElementById('hours');
var o_minutes = document.getElementById('minutes');
var o_seconds = document.getElementById('seconds');
//倒计时
var timer = setInterval(function(){
//1. 现在时间
var now = new Date();
//2. 两点到期
var date = new Date('2022/12/9 14:00:00');
//3. 求时间差
var minus = Math.floor((date.getTime() - now.getTime()) / 1000);
//4. 求小时
var hours = Math.floor(minus / 60 / 60);
//5. 求分
var minutes = Math.floor((minus - hours * 60 * 60) / 60);
//6. 求秒
var seconds = minus % 60;
console.log(hours + ':' + minutes + ':' + seconds);
o_hours.value = hours;
o_minutes.value = minutes;
o_seconds.value = seconds;
//7. 倒计时的停止条件
if(hours === 0 && minutes === 0 && seconds === 0){
clearInterval(timer);
alert('倒计时结束!');
}
},1000)
</script>
</body>
</html>简单的代码异步执行机制同步:按步骤执行异步:同时进行计算机程序执行分为同步执行,和异步执行:所谓的异步执行,是一种特殊的程序的执行方式,常见的异步程序有:定时器(setInterval),延时器(setTimeou),各种事件的绑定(onclick......),ajax请求2. 异步程序的执行过程从第一行代码开始执行同步程序开始执行遇到异步程序了,暂时不执行,将异步程序暂时存储在“异步池”中所有的同步程序执行完毕开始执行“异步池”中的异步程序若有设定了时间的程序,就会先执行到点了的程序若有设定的时间是相同的程序,则依照书写顺序执行setTimeout(function(){
console.log('我是异步执行的程序1');
} , 2000);
setTimeout(function(){
console.log('我是异步执行的程序2');
} , 1000);
console.log('我是同步执行的程序')
//结果依次是:
//我是同步执行的程序
//我是异步执行的程序2
//我是异步执行的程序1
Ned
深入了解JavaScript事件绑定:实现高效可靠的事件处理
事件绑定方式什么是事件一个事件由什么东西组成触发谁的事件:事件源触发什么事件:事件类型触发以后做什么:事件处理函数var oDiv = document.querySelector('div')
oDiv.onclick = function () {}
// 谁来触发事件 => oDiv => 这个事件的事件源就是 oDiv
// 触发什么事件 => onclick => 这个事件类型就是 click
// 触发之后做什么 => function () {} => 这个事件的处理函数我们想要在点击 div 以后做什么事情,就把我们要做的事情写在事件处理函数里面var oDiv = document.querySelector('div')
oDiv.onclick = function () {
console.log('你点击了 div')
}当我们点击 div 的时候,就会执行事件处理函数内部的代码每点击一次,就会执行一次事件处理函数DOM0级 事件1.常用事件onblur : 失焦事件onfocus : 得焦事件onchange : 内容改变事件2.鼠标常用事件onclick : 点击事件ondblclick : 双击事件onmousedown : 鼠标按下事件onmouseup : 鼠标抬起事件onmouseenter : 鼠标移入事件onmouseleave : 鼠标移出事件onmouseover : 鼠标移入事件onmouseout : 鼠标移出事件onmousemove : 鼠标移动事件3.键盘常用事件onkeydown : 键盘按下onkeyup : 键盘抬起onkeypress : 键盘按过(按下时触发)4.其它事件onload : 加载成功事件error : 加载失败事件resize : 大小改变事件 <script>
//1. 获取元素对象
var txt = document.querySelector('input');
var box = document.querySelector('#box');
//2. 添加事件
txt.onfocus = function(){
this.value = ''; //清空当前文本框
}
txt.onblur = function(){
alert('失焦后:' + this.value);
}
txt.onblur = null;
txt.onchange = function(){
alert('改变后:' + this.value);
}
txt.oninput = function(){
box.innerText = this.value;
}
box.onclick = function(){
console.log('点击了div');
}
box.ondblclick = function(){
console.log('双击了div')
}
box.onmousedown = function(){
console.log('鼠标在div上按下');
}
box.onmouseup = function(){
console.log('鼠标在div上抬起');
}
box.onmouseover = function(){
console.log('over:','鼠标移入了div中');
}
box.onmouseout = function(){
console.log('out:' + '鼠标移出了div');
}
box.onmouseenter = function(){
console.log('enter:' + '鼠标移入了div');
}
box.onmouseleave = function(){
console.log('leave:鼠标移出了div');
}
box.onmousemove = function(){
console.log('鼠标在div上移动');
}
document.onkeydown = function(){
console.log('按下键盘');
}
document.onkeyup = function(){
console.log('抬起键盘')
}
document.onkeypress = function(){
console.log('按过键盘');
}
</script>DOM0级事件绑定ele.onclick = function(){}DOM2级 事件绑定标准 浏览器: ele.addEventListener('click',function(){},false)IE 浏览器 :ele.attachEvent('onclick',function(){}) <script>
//1. 获取元素
var box = document.querySelector('#box');
function fn1(){
alert('开灯');
}
function fn2(){
alert('开空调');
}
function fn3(){
alert('开热水器');
}
2. 绑定事件
box.onclick = fn1;
box.onclick = fn2;
box.onclick = fn3;
标准浏览器添加事件监听器
1. 第一个参数:事件类型
2. 第二个参数:事件处理程序
3. 第三个参数:是否进行捕获 false(默认) : 冒泡 true(捕获)
box.addEventListener('click',fn1,false);
box.addEventListener('click',fn2,false);
box.addEventListener('click',fn3,false);
IE浏览器添加事件监听器
1. 第一个参数:事件驱动
2. 第二个参数:事件处理程序
box.attachEvent('onclick',fn1);
box.attachEvent('onclick',fn2);
box.attachEvent('onclick',fn3);
兼容
function addEventListener(dom,type,fn,bool){
//1. 处理默认参数的问题
bool = bool || false;
//2. 是否支持addEventListener方法
if(dom.addEventListener){
dom.addEventListener(type,fn,bool);
}else if(dom.attachEvent){
dom.attachEvent('on' + type,fn);
}
}
addEventListener(box,'click',fn1);
addEventListener(box,'click',fn2);
addEventListener(box,'click',fn3);
//标准浏览器移除事件监听器
// box.removeEventListener('click',fn2,false);
//IE浏览器移除事件监听器
// box.detachEvent('onclick',fn2);
function removeEventListener(dom,type,fn,bool){
//1. 处理默认参数的问题
bool = bool || false;
//2. 是否支持addEventListener方法
if(dom.removeEventListener){
dom.removeEventListener(type,fn,bool);
}else if(dom.detachEvent){
dom.detachEvent('on' + type,fn);
}
}
removeEventListener(box,'click',fn2);
</script>//添加事件监听器
//标准浏览器
//第一个参数:事件类型 (名词)
//第二个参数:事件处理程序
//第三个参数:是否进行事件捕获? false(冒泡-默认值) true(捕获)
// o_btn.addEventListener('click',fn1,false);
// o_btn.addEventListener('click',fn2,false);
// o_btn.addEventListener('click',fn3,false);
//IE浏览器
第一个参数:事件驱动
第二个参数:事件处理程序
// o_btn.attachEvent('onclick',fn1);
// o_btn.attachEvent('onclick',fn2);
// o_btn.attachEvent('onclick',fn3);
//兼容
function addEventListener(obj,type,fn,bool){
//初始化bool参数
bool = bool || false;
if(obj.addEventListener){
obj.addEventListener(type,fn,bool);
}else if(obj.attachEvent){
obj.attachEvent('on' + type,fn);
}
}事件解绑方式DOM0级事件解绑ele.onclick = null;DOM2级事件解绑标准 浏览器: ele.removeEventListener('click',function(){},false)IE 浏览器 :ele.detachEvent('onclick',function(){})//移除事件监听
//标准浏览器
// o_btn.removeEventListener('click',fn2,false);
//IE浏览器
// o_btn.detachEvent('onclick',fn2);
//兼容
function removeEventListener(obj,type,fn,bool){
//初始化bool参数
bool = bool || false;
if(obj.removeEventListener){
obj.removeEventListener(type,fn,bool);
}else if(obj.detachEvent){
obj.detachEvent('on' + type,fn);
}
}事件对象的认识什么是事件对象?(类似于飞机上的黑匣子、汽车上的行车记录仪、教室中的摄像头)当绑定事件的对象,在触发事件时,所发生的事情记录在一个地方,而这个地方称为事件对象。如何获取事件对象?标准浏览器获取事件对象的方式: 触发事件后,会自动给事件处理程序传递一个参数,这个参数就是事件对象。IE浏览器获取事件对象的方式: window.event兼容:arguments : 内置的对象,是一个伪数组,是实参副本.function getEvent(){
return arguments[0] || window.event;
}事件对象内鼠标相关信息鼠标的相对坐标值:(鼠标相对于所在对象上的坐标值)event.offsetX / event.offsetY鼠标的可视区坐标值:(当前鼠标触发点距离当前窗口左上角的X/Y坐标)event.clientX / event.clientY鼠标的绝对坐标值:(当前鼠标触发点距离BODY左上角的X/Y坐标(IE9以下没有这两个属性)//兼容处理:
event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft)
event.clientY + (document.documentElement.scrollTop || document.body.scrollTop)事件对象内键盘相关信息onkeydown/onkeyup : event.keyCode监听整个键盘,其中字母键只返回大写的编码值。onkeypress : event.keyCode || event.charCode || event.which监听编辑键区,字母键返回大小的编码值。 低版本浏览器中有可能监听功能键区、回车键出现过10 兼容://7. 获取键盘编码值的兼容
function getKeyCode(evt){
var e = evt || window.event;
return e.keyCode || e.charCode || e.which;
}altKey / shiftKey / ctrlKey / metakeyevent.metaKey : 返回一个布尔值标识meta键(windows键)是否被按键并被保持。返回true表示meta键按下并保持返回false表示没有满足meta键按下并保持的情况。
Ned
探索JavaScript对象的无限可能性:构建复杂应用的基石
对象详解Object.defineProperty1.Object.defineProperty(obj, prop, descriptor) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。obj : 要定义属性的对象。prop : 要定义或修改的属性的名称。descriptor : 要定义或修改的属性描述符。2.对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。3数据描述符:configurable:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性,默认值为false。var obj = {
name : "张三"
}
Object.defineProperty(obj,"name",{
configurable : false
})
console.log(obj); //{name : "张三"}
delete obj.name;
console.log(obj); //{name : "张三"}2. enumerable:表示能否通过for in循环访问属性,默认值为false3. writable:表示能否修改属性的值。默认值为false。var obj = {
name : "张三"
}
Object.defineProperty(obj,'age',{
writable : false,
value : 18
})
console.log(obj.age); //18
obj.age = 20;
console.log(obj.age); //184. value:包含这个属性的数据值。默认值为undefined。5.存取描述符get:getter在读取属性时调用的函数,默认值是undefinedset:setter在写入属性的时候调用的函数,默认值是undefined注 : setter 不能和writable 、value 一起使用。var obj = {
_year : 2022
}
Object.defineProperty(obj,'year',{
get : function(){
return this._year;
},
set : function(yyyy){
if(yyyy > 2022){
this._year = yyyy;
}
}
})
obj.year = 2023;
console.log(obj._year);定义多个属性var student = {};
var obj = {};
Object.defineProperties(student,{
name: {
writeble : false,
value: "张三"
},
age : {
writeble: true,
value: 16
},
sex: {
get(){
return '男';
},
set(v){
obj.sex = v;
}
}
})
obj.sex = '男';
console.log(student.name + ':' + student.age); //张三:16
console.log(obj.sex); //男
student.sex = '女';
console.log(student.sex); //男
console.log(obj.sex); //女Proxy概念:Proxy : ES6提供的数据代理,表示由它来“代理”某些操作,可以称为“代理器"2. 语法:new Proxy(代理原始对象,{配置项}) : 返回的实例对象,就是代理结果数据new Proxy()表示生成一个Proxy实例代理原始对象: 要被代理的对象,可以是一个object或者function配置项:也是一个对象,对该代理对象的各种操作行为处理。Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写let obj = {name: '张三',age: 18};
//开始代理
let result = new Proxy(obj,{
//配置get来进行代理设置
get(target,property){
//target: 就是你要代理的目标对象,我们当前是obj
//property: 就是该对象内的每一个属性,自动遍历
return target[property];
},
//配置set来进行修改
set(target,property,val){
//target: 就是你要代理的目标对象,我们当前是obj
//property: 就是该对象内的你要修改的那个属性
//val: 就是你要修改的那个属性的值
target[property] = val;
console.log('你在修改' + property + '属性,你想修改为:' + val);
//注意:简单代理需要返回 true
return true;
}
})
console.log('原始数据:' + obj);
console.log('代理结果:' + result);
console.log('代理结果 name : ' + result.name);
//尝试修改
result.name = '李四';
console.log('代理结果 name: ' + result .name);
//动态插入
result.sex = '男';
console.log('代理结果 sex:' + result.sex);hasOwnPropertyhasOwnProperty : 表示是否有自己的属性。这个方法会查找一个对象是否有某个属性,但是不会去查找它的原型链。let obj = {
a : 1,
fn : function(){},
c : {
d : 2
}
}
console.log(obj.hasOwnProperty('a')); //true
console.log(obj.hasOwnProperty('fn')); //true
console.log(obj.hasOwnProperty('c')); //true
console.log(obj.c.hasOwnProperty('d')); //true
console.log(obj.hasOwnProperty('d')); //false
let str = new String();
console.log(str.hasOwnProperty('charAt')); //false
console.log(String.prototype.hasOwnProperty('charAt')); //true判断自身属性是否存在let obj = new Object(); //创建一个空对象
obj.name = '张三'; //添加一个name属性
//备份一份旧的name属性,然后删除旧属性
function changeObj(){
obj.newname = obj.name;
delete obj.name;
}
console.log(obj.hasOwnProperty('name')); //true
changeObj();
console.log(obj.hasOwnProperty('name')); //false判断自身属性与继承属性function foo() {
this.name = 'foo'
this.sayHi = function () {
console.log('Say Hi')
}
}
foo.prototype.sayGoodBy = function () {
console.log('Say Good By')
}
let myPro = new foo()
console.log(myPro.name) // foo
console.log(myPro.hasOwnProperty('name')) // true
console.log(myPro.hasOwnProperty('toString')) // false
console.log(myPro.hasOwnProperty('hasOwnProperty')) // false
console.log(myPro.hasOwnProperty('sayHi')) // true
console.log(myPro.hasOwnProperty('sayGoodBy')) // false
console.log('sayGoodBy' in myPro) // true
遍历一个对象的所有自身属性:使用hasOwnProperty()方法来忽略继承属性。var buz = {
a: 1
};
for (var name in buz) {
if (buz.hasOwnProperty(name)) {
alert(name + ':' + buz[name]);
}
else {
alert(name);
}
}深浅拷贝针对引用类型而言,浅拷贝指的是复制对象的引用,即直接给引用类型赋值,如果拷贝后的对象发生变化,原对象也会发生变化。而深拷贝是真正地对对象进行拷贝,修改拷贝后的新对象并不会对原对象产生任何影响。在JS中数据类型分为基本类型和引用类型 基本类型: number, boolean,string,symbol,undefined,null引用类型:object 以及一些标准内置对象 Array、RegExp、String、Map、Set…基本类型数据拷贝基本类型数据都是值类型,存储在栈内存中,每次赋值都是一次复制的过程var a = 12;
var b = a;
console.log(a,b); //12 12
a = 13;
console.log(a,b); //13 12引用类型数据拷贝只拷贝对象的一层数据,再深处层次的引用类型value将只会拷贝引用浅拷贝let obj = {
a : 1,
b : {
c : 2,
d : {
f : 3
}
}
}
let newObj = {... obj};
console.log(newObj);
obj.b.c = 10;
console.log(newObj);function cloneObj(obj){
let clone = {};
for(let key in obj){
clone[key] = obj[key];
}
return clone;
}
let a = {
x : {
y : 1
}
}
let b = cloneObj(a);
console.log(a,b);深拷贝深拷贝就不会像浅拷贝那样只拷贝一层,而是有多少层我就拷贝多少层,要真正的做到全部内容都放在自己新开辟的内存里。可以利用递归思想实现深拷贝。function cloneObj(obj) {
let clone = {};
for (let i in obj) {
// 如果为对象则递归更进一层去拷贝
if (typeof obj[i] == "object" && obj[i] != null) {
clone[i] = cloneObj(obj[i]);
} else {
clone[i] = obj[i];
}
}
return clone;
}
Ned
JavaScript中的数据缓存与内存泄露:解密前端性能优化与代码健康
说说你对事件循环的理解一、是什么首先,JavaScript是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环在JavaScript中,所有的任务都可以分为同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行异步任务:异步执行的任务,比如ajax网络请求,setTimeout定时函数等同步任务与异步任务的运行流程图如下:从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就事件循环二、宏任务与微任务如果将任务划分为同步任务和异步任务并不是那么的准确,举个例子:console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)如果按照上面流程图来分析代码,我们会得到下面的执行步骤:console.log(1),同步任务,主线程中执行setTimeout() ,异步任务,放到 Event Table,0 毫秒后console.log(2)回调推入 Event Queue 中new Promise ,同步任务,主线程直接执行.then ,异步任务,放到 Event Tableconsole.log(3),同步任务,主线程执行所以按照分析,它的结果应该是 1 => 'new Promise' => 3 => 2 => 'then'但是实际结果是:1=>'new Promise'=> 3 => 'then' => 2出现分歧的原因在于异步任务执行顺序,事件队列其实是一个“先进先出”的数据结构,排在前面的事件会优先被主线程读取例子中 setTimeout回调事件是先进入队列中的,按理说应该先于 .then 中的执行,但是结果却偏偏相反原因在于异步任务还可以细分为微任务与宏任务微任务一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前常见的微任务有:Promise.thenMutaionObserverObject.observe(已废弃;Proxy 对象替代)process.nextTick(Node.js)宏任务宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合常见的宏任务有:script (可以理解为外层同步代码)setTimeout/setIntervalUI rendering/UI事件postMessage、MessageChannelsetImmediate、I/O(Node.js)这时候,事件循环,宏任务,微任务的关系如图所示按照这个流程,它的执行机制是:执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完回到上面的题目console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)流程如下// 遇到 console.log(1) ,直接打印 1
// 遇到定时器,属于新的宏任务,留着后面执行
// 遇到 new Promise,这个是直接执行的,打印 'new Promise'
// .then 属于微任务,放入微任务队列,后面再执行
// 遇到 console.log(3) 直接打印 3
// 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 'then'
// 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2三、async与awaitasync 是异步的意思,await则可以理解为 async wait。所以可以理解async就是用来声明一个异步方法,而 await是用来等待异步方法执行asyncasync函数返回一个promise对象,下面两种方法是等效的function f() {
return Promise.resolve('TEST');
}
// asyncF is equivalent to f!
async function asyncF() {
return 'TEST';
}await正常情况下,await命令后面是一个 Promise对象,返回该对象的结果。如果不是 Promise对象,就直接返回对应的值async function f(){
// 等同于
// return 123
return await 123
}
f().then(v => console.log(v)) // 123不管await后面跟着的是什么,await都会阻塞后面的代码async function fn1 (){
console.log(1)
await fn2()
console.log(2) // 阻塞
}
async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)上面的例子中,await 会阻塞下面的代码(即加入微任务队列),先执行 async外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码所以上述输出结果为:1,fn2,3,2四、流程分析通过对上面的了解,我们对JavaScript对各种场景的执行顺序有了大致的了解这里直接上代码:async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('settimeout')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')分析过程:执行整段代码,遇到 console.log('script start') 直接打印结果,输出 script start遇到定时器了,它是宏任务,先放着不执行遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到await怎么办?先执行 async2,打印 async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await下面的代码,打印 async1 end继续执行下一个微任务,即执行 then 的回调,打印 promise2上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout所以最后的结果是:script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout说说 JavaScript 中内存泄漏的几种情况?一、是什么内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃在C语言中,因为是手动管理内存,内存泄露是经常出现的事情。 char * buffer;
buffer = (char*) malloc(42);
// Do something with buffer
free(buffer);上面是 C 语言代码,malloc方法用来申请内存,使用完毕之后,必须自己用free方法释放内存。这很麻烦,所以大多数语言提供自动内存管理,减轻程序员的负担,这被称为"垃圾回收机制"二、垃圾回收机制Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存通常情况下有两种实现方式:标记清除引用计数标记清除JavaScript最常用的垃圾收回机制当变量进入执行环境是,就标记这个变量为“进入环境“。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为“离开环境“垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存举个例子: 引用计数语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏const arr = [1, 2, 3, 4];
console.log('hello world');上面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存如果需要这块内存被垃圾回收机制释放,只需要设置如下:arr = null通过设置arr为null,就解除了对数组[1,2,3,4]的引用,引用次数变为 0,就被垃圾回收了小结有了垃圾回收机制,不代表不用关注内存泄露。那些很占空间的值,一旦不再用到,需要检查是否还存在对它们的引用。如果是的话,就必须手动解除引用三、常见内存泄露情况意外的全局变量function foo(arg) {
bar = "this is a hidden global variable";
}另一种意外的全局变量可能由 this 创建:function foo() {
this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();上述使用严格模式,可以避免意外的全局变量定时器也常会造成内存泄露var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放包括我们之前所说的闭包,维持函数内局部变量,使其得不到释放function bindEvent() {
var obj = document.createElement('XXX');
var unused = function () {
console.log(obj, '闭包内引用obj obj不会被释放');
};
obj = null; // 解决方法
}没有清理对DOM元素的引用同样造成内存泄露const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom删除了
console.log(refA, 'refA'); // 但是还存在引用能console出整个div 没有被回收
refA = null;
console.log(refA, 'refA'); // 解除引用包括使用事件监听addEventListener监听的时候,在不监听的情况下使用removeEventListener取消对事件监听
Ned
从零开始:手写 JavaScript 代码应用于实际场景
场景应用1. 循环打印红黄绿下面来看一道比较典型的问题,通过这个问题来对比几种异步编程方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?三个亮灯函数:function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
复制代码这道题复杂的地方在于需要“交替重复”亮灯,而不是“亮完一次”就结束了。(1)用 callback 实现const task = (timer, light, callback) => {
setTimeout(() => {
if (light === 'red') {
red()
}
else if (light === 'green') {
green()
}
else if (light === 'yellow') {
yellow()
}
callback()
}, timer)
}
task(3000, 'red', () => {
task(2000, 'green', () => {
task(1000, 'yellow', Function.prototype)
})
})
复制代码这里存在一个 bug:代码只是完成了一次流程,执行后红黄绿灯分别只亮一次。该如何让它交替重复进行呢?上面提到过递归,可以递归亮灯的一个周期:const step = () => {
task(3000, 'red', () => {
task(2000, 'green', () => {
task(1000, 'yellow', step)
})
})
}
step()
复制代码注意看黄灯亮的回调里又再次调用了 step 方法 以完成循环亮灯。(2)用 promise 实现const task = (timer, light) =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red()
}
else if (light === 'green') {
green()
}
else if (light === 'yellow') {
yellow()
}
resolve()
}, timer)
})
const step = () => {
task(3000, 'red')
.then(() => task(2000, 'green'))
.then(() => task(2100, 'yellow'))
.then(step)
}
step()
复制代码这里将回调移除,在一次亮灯结束后,resolve 当前 promise,并依然使用递归进行。(3)用 async/await 实现const taskRunner = async () => {
await task(3000, 'red')
await task(2000, 'green')
await task(2100, 'yellow')
taskRunner()
}
taskRunner()
复制代码2. 实现每隔一秒打印 1,2,3,4// 使用闭包实现
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
// 使用 let 块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
复制代码3. 小孩报数问题有30个小孩儿,编号从1-30,围成一圈依此报数,1、2、3 数到 3 的小孩儿退出这个圈, 然后下一个小孩 重新报数 1、2、3,问最后剩下的那个小孩儿的编号是多少?function childNum(num, count){
let allplayer = [];
for(let i = 0; i < num; i++){
allplayer[i] = i + 1;
}
let exitCount = 0; // 离开人数
let counter = 0; // 记录报数
let curIndex = 0; // 当前下标
while(exitCount < num - 1){
if(allplayer[curIndex] !== 0) counter++;
if(counter == count){
allplayer[curIndex] = 0;
counter = 0;
exitCount++;
}
curIndex++;
if(curIndex == num){
curIndex = 0
};
}
for(i = 0; i < num; i++){
if(allplayer[i] !== 0){
return allplayer[i]
}
}
}
childNum(30, 3)
复制代码4. 用Promise实现图片的异步加载let imageAsync=(url)=>{
return new Promise((resolve,reject)=>{
let img = new Image();
img.src = url;
img.οnlοad=()=>{
console.log(`图片请求成功,此处进行通用操作`);
resolve(image);
}
img.οnerrοr=(err)=>{
console.log(`失败,此处进行失败的通用操作`);
reject(err);
}
})
}
imageAsync("url").then(()=>{
console.log("加载成功");
}).catch((error)=>{
console.log("加载失败");
})
复制代码5. 实现发布-订阅模式class EventCenter{
// 1. 定义事件容器,用来装事件数组
let handlers = {}
// 2. 添加事件方法,参数:事件名 事件方法
addEventListener(type, handler) {
// 创建新数组容器
if (!this.handlers[type]) {
this.handlers[type] = []
}
// 存入事件
this.handlers[type].push(handler)
}
// 3. 触发事件,参数:事件名 事件参数
dispatchEvent(type, params) {
// 若没有注册该事件则抛出错误
if (!this.handlers[type]) {
return new Error('该事件未注册')
}
// 触发事件
this.handlers[type].forEach(handler => {
handler(...params)
})
} // 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和发布 removeEventListener(type, handler) {
if (!this.handlers[type]) {
return new Error('事件无效')
}
if (!handler) {
// 移除事件
delete this.handlers[type]
} else {
const index = this.handlers[type].findIndex(el => el === handler)
if (index === -1) {
return new Error('无该绑定事件')
}
// 移除事件
this.handlers[type].splice(index, 1)
if (this.handlers[type].length === 0) {
delete this.handlers[type]
}
}
}
}
复制代码6. 查找文章中出现频率最高的单词function findMostWord(article) {
// 合法性判断
if (!article) return;
// 参数处理
article = article.trim().toLowerCase();
let wordList = article.match(/[a-z]+/g),
visited = [],
maxNum = 0,
maxWord = "";
article = " " + wordList.join(" ") + " ";
// 遍历判断单词出现次数
wordList.forEach(function(item) {
if (visited.indexOf(item) < 0) {
// 加入 visited
visited.push(item);
let word = new RegExp(" " + item + " ", "g"),
num = article.match(word).length;
if (num > maxNum) {
maxNum = num;
maxWord = item;
}
}
});
return maxWord + " " + maxNum;
}
复制代码7. 封装异步的fetch,使用async await方式来使用(async () => {
class HttpRequestUtil {
async get(url) {
const res = await fetch(url);
const data = await res.json();
return data;
}
async post(url, data) {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await res.json();
return result;
}
async put(url, data) {
const res = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(data)
});
const result = await res.json();
return result;
}
async delete(url, data) {
const res = await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify(data)
});
const result = await res.json();
return result;
}
}
const httpRequestUtil = new HttpRequestUtil();
const res = await httpRequestUtil.get('http://golderbrother.cn/');
console.log(res);
})();8. 实现prototype继承所谓的原型链继承就是让新实例的原型等于父类的实例://父方法
function SupperFunction(flag1){
this.flag1 = flag1;
}
//子方法
function SubFunction(flag2){
this.flag2 = flag2;
}
//父实例
var superInstance = new SupperFunction(true);
//子继承父
SubFunction.prototype = superInstance;
//子实例
var subInstance = new SubFunction(false);
//子调用自己和父的属性
subInstance.flag1; // true
subInstance.flag2; // false
复制代码9. 实现双向数据绑定let obj = {}
let input = document.getElementById('input')
let span = document.getElementById('span')
// 数据劫持
Object.defineProperty(obj, 'text', {
configurable: true,
enumerable: true,
get() {
console.log('获取数据了')
},
set(newVal) {
console.log('数据更新了')
input.value = newVal
span.innerHTML = newVal
}
})
// 输入监听
input.addEventListener('keyup', function(e) {
obj.text = e.target.value
})
复制代码10. 实现简单路由// hash路由
class Route{
constructor(){
// 路由存储对象
this.routes = {}
// 当前hash
this.currentHash = ''
// 绑定this,避免监听时this指向改变
this.freshRoute = this.freshRoute.bind(this)
// 监听
window.addEventListener('load', this.freshRoute, false)
window.addEventListener('hashchange', this.freshRoute, false)
}
// 存储
storeRoute (path, cb) {
this.routes[path] = cb || function () {}
}
// 更新
freshRoute () {
this.currentHash = location.hash.slice(1) || '/'
this.routes[this.currentHash]()
}
}
复制代码11. 实现斐波那契数列// 递归
function fn (n){
if(n==0) return 0
if(n==1) return 1
return fn(n-2)+fn(n-1)
}
// 优化
function fibonacci2(n) {
const arr = [1, 1, 2];
const arrLen = arr.length;
if (n <= arrLen) {
return arr[n];
}
for (let i = arrLen; i < n; i++) {
arr.push(arr[i - 1] + arr[ i - 2]);
}
return arr[arr.length - 1];
}
// 非递归
function fn(n) {
let pre1 = 1;
let pre2 = 1;
let current = 2;
if (n <= 2) {
return current;
}
for (let i = 2; i < n; i++) {
pre1 = pre2;
pre2 = current;
current = pre1 + pre2;
}
return current;
}
复制代码12. 字符串出现的不重复最长长度用一个滑动窗口装没有重复的字符,枚举字符记录最大值即可。用 map 维护字符的索引,遇到相同的字符,把左边界移动过去即可。挪动的过程中记录最大长度:var lengthOfLongestSubstring = function (s) {
let map = new Map();
let i = -1
let res = 0
let n = s.length
for (let j = 0; j < n; j++) {
if (map.has(s[j])) {
i = Math.max(i, map.get(s[j]))
}
res = Math.max(res, j - i)
map.set(s[j], j)
}
return res
};
复制代码13. 使用 setTimeout 实现 setIntervalsetInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当当前的执行栈为空的时候,才能去从事件队列中取出事件执行。所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能到间隔一段时间执行的效果。针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,这样解决了 setInterval 的问题。实现思路是使用递归函数,不断地去执行 setTimeout 从而达到 setInterval 的效果function mySetInterval(fn, timeout) {
// 控制器,控制定时器是否继续执行
var timer = {
flag: true
};
// 设置递归函数,模拟定时器执行。
function interval() {
if (timer.flag) {
fn();
setTimeout(interval, timeout);
}
}
// 启动定时器
setTimeout(interval, timeout);
// 返回控制器
return timer;
}15. 判断对象是否存在循环引用循环引用对象本来没有什么问题,但是序列化的时候就会发生问题,比如调用JSON.stringify()对该类对象进行序列化,就会报错: Converting circular structure to JSON.下面方法可以用来判断一个对象中是否已存在循环引用:const isCycleObject = (obj,parent) => {
const parentArr = parent || [obj];
for(let i in obj) {
if(typeof obj[i] === 'object') {
let flag = false;
parentArr.forEach((pObj) => {
if(pObj === obj[i]){
flag = true;
}
})
if(flag) return true;
flag = isCycleObject(obj[i],[...parentArr,obj[i]]);
if(flag) return true;
}
}
return false;
}
const a = 1;
const b = {a};
const c = {b};
const o = {d:{a:3},c}
o.c.b.aa = a;
console.log(isCycleObject(o)查找有序二维数组的目标值:var findNumberIn2DArray = function(matrix, target) {
if (matrix == null || matrix.length == 0) {
return false;
}
let row = 0;
let column = matrix[0].length - 1;
while (row < matrix.length && column >= 0) {
if (matrix[row][column] == target) {
return true;
} else if (matrix[row][column] > target) {
column--;
} else {
row++;
}
}
return false;
};二维数组斜向打印:function printMatrix(arr){
let m = arr.length, n = arr[0].length
let res = []
// 左上角,从0 到 n - 1 列进行打印
for (let k = 0; k < n; k++) {
for (let i = 0, j = k; i < m && j >= 0; i++, j--) {
res.push(arr[i][j]);
}
}
// 右下角,从1 到 n - 1 行进行打印
for (let k = 1; k < m; k++) {
for (let i = k, j = n - 1; i < m && j >= 0; i++, j--) {
res.push(arr[i][j]);
}
}
return res
}
Ned
用Three.js搞个炫酷3D地球
1.用canvas画一张地球贴图1.地球geojsonnpm i @surbowl/world-geo-json-zh 2.画出地球贴图众所周知,经度范围[-180,180],纬度范围[-90,90]那么显而易见,经度是维度的两倍长度,所以画出的canvas也是2:1的图片,为了方便计算我这里将经纬度分别放大十倍画图,即宽度360*10,高度180*101.canvas样式设置 let canvas = document.createElement('canvas');
canvas.width = 3600;
canvas.height = 1800;
let ctx = canvas.getContext('2d');
//背景颜色
ctx.fillStyle = that.bg;
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.fill();
//设置地图样式
ctx.strokeStyle = that.borderColor;//描边颜色
ctx.lineWidth = that.borderWidth;//描边线宽
ctx.fillStyle = that.fillColor;//填充颜色
if (that.blurWidth) {
ctx.shadowBlur = that.blurWidth;//边界模糊范围
ctx.shadowColor = that.blurColor;//边界模糊颜色
}2.遍历geojson画区块geojson格式中features数组里面每个geometry包含了区块形状的坐标,对其进行遍历,分别画出区块就行 fetch('../node_modules/@surbowl/world-geo-json-zh/world.zh.json')
.then((res) => res.json())
.then((geojson) => {
console.log(geojson);
geojson.features.forEach((a) => {
if (a.geometry.type == 'MultiPolygon') {//多个区块组成
a.geometry.coordinates.forEach((b) => {
b.forEach((c) => {
drawRegion(ctx, c);
});
});
} else {//单个区块
a.geometry.coordinates.forEach((c) => {
drawRegion(ctx, c);
});
}
});
document.body.appendChild(canvas);
});画区块: 对每一组坐标点进行遍历,第一个点用moveTo,后面的点全用lineTo,为了保证每个区块形状都是独立的闭合形状,记得开始要用beginPath,结束要用closePath注意:在canvas中坐标是以左上角的原点开始的,所以经纬度在canvas坐标是不适用的,需要转换,因为canvas的y轴正方向与纬度的方向是相反的,所以纬度需要取负值。-lat而经纬度有正负值,为了保证所有坐标都落在canvas可视范围内,要将坐标全部向canvas正轴方向偏移,经度偏移180度,纬度偏移90度,即lng+180,-lat+90function drawRegion(ctx, c, geoInfo) {
ctx.beginPath();
c.forEach((item, i) => {
//转换经纬度坐标为canvas坐标点
let pos = [(item[0] + 180)*10, (-item[1] + 90)*10];
if (i == 0) {
ctx.moveTo(pos[0], pos[1]);
} else {
ctx.lineTo(pos[0], pos[1]);
}
});
ctx.closePath();
ctx.fill();
ctx.stroke();
}3.使用canvas画地球贴图 var that = {
bg: '#000080',//背景色
borderColor: '#1E90FF',//描边颜色
blurColor: '#1E90FF',//边界模糊颜色
borderWidth: 1,//描边宽度
blurWidth: 5,//边界模糊范围
fillColor: 'rgb(30 ,144 ,255,0.3)',//区块填充颜色
};如图所见,虽然没了北冰洋但是还算个完整的地球贴图2.添加一个地球用刚才画出来的canvas地球贴图作为球体的材质就能得到一个地球了//地球canvas贴图
const map = new THREE.CanvasTexture(canvas);
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
//球体
const geometry = new THREE.SphereGeometry(1, 32, 32);
const material = new THREE.MeshBasicMaterial({ map: map, transparent: true });
const sphere = new THREE.Mesh(geometry, material);
this.scene.add(sphere);
3.在地球上添加柱体1.计算柱体位置柱体要相对于地球表面进行旋转延伸,如果用球坐标系计算有点麻烦,而THREE有个很好的属性,对象是层级结构的,子级对象的位置和朝向都是相对于父级对象的。比如月亮绕着地球转(月亮是地球的子级),地球绕着太阳转的(地球是太阳的子级),只需要设置太阳系自转和地球自转,就可以做到。那么我们可以利用这个属性,添加几个层级结构的3D对象,作为辅助对象,通过操作3D对象的变换,获取其变换矩阵作为柱体变换矩阵即可。 const lonHelper = new THREE.Object3D();//经度旋转辅助对象
this.scene.add(lonHelper);
const latHelper = new THREE.Object3D();//维度旋转辅助对象
lonHelper.add(latHelper);
const positionHelper = new THREE.Object3D();//最终位置辅助对象
positionHelper.position.z = 1;//球体半径是1,让变换位置在球体表面,需z坐标向外偏移1
latHelper.add(positionHelper);柱体在球面的经纬度的位置为,经度辅助对象先绕y轴旋转对应经度,然后维度辅助对象绕x轴旋转对应维度,即位置辅助对象当前变换矩阵就是柱体的最终位置的变换矩阵。假设辅助对象变成了长方体,我们可以观察它的变化
//经度旋转辅助对象
const lonHelper = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 0.5, 2),
new THREE.MeshBasicMaterial({ color: '#FF0000' })
);
this.scene.add(lonHelper);
//维度旋转辅助对象
const latHelper = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 0.5, 1),
new THREE.MeshBasicMaterial({ color: '#0000FF' })
);
lonHelper.add(latHelper);
//最终位置辅助对象
const positionHelper = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 0.5, 0.5),
new THREE.MeshBasicMaterial({ color: '#00FF00' })
);
positionHelper.position.z = 1;
latHelper.add(positionHelper);
const gui = new dat.GUI();
gui.add(lonHelper.rotation, 'y', -Math.PI, Math.PI);
gui.add(latHelper.rotation, 'x', -Math.PI * 0.5, Math.PI * 0.5);
绿色小正方体的位置就是柱体的位置2.添加柱体默认柱体形状是1的单位长度,因为柱体的底面要贴着地球表面,因此要将柱体中心点往外偏移0.5,让中心点落在底面上。 const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
boxGeometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0, 0, 0.5));计算柱体热力颜色: 采用HSL颜色模式色相(hue):色轮上从 0 到 360 的度数。0 是红色,120 是绿色,240 是蓝色。饱和度(saturation):百分比,0% 表示灰色阴影,而 100% 是全色。亮度(lightness)百分比,0% 是黑色,50% 是既不明也不暗,100%是白色通过MathUtils.lerp线性插值来计算对应值要取什么颜色值 const amount = (value - this.min) / (this.max-this.min);
//色相
const hue = THREE.MathUtils.lerp(this.that.barHueStart, this.that.barHueEnd, amount);
//饱和度
const saturation = 1;
//亮度
const lightness = THREE.MathUtils.lerp(
this.that.barLightStart,
this.that.barLightEnd,
amount
);
material.color.setHSL(hue, saturation, lightness);
注意:HSL颜色到THREE.color中要转换成[0,1]范围的值const mesh = new THREE.Mesh(geometry, material);
this.scene.add(mesh);
//旋转经度
lonHelper.rotation.y = THREE.MathUtils.degToRad(lon);
//旋转维度
latHelper.rotation.x = THREE.MathUtils.degToRad(lat);
//最终坐标位置
positionHelper.updateWorldMatrix(true, false);
mesh.applyMatrix4(positionHelper.matrixWorld);
//柱体高度跟着数值变化
mesh.scale.set(0.01, 0.01, THREE.MathUtils.lerp(0.01, 0.5, amount));数据来源于antV L7示例数据:全球地震热力分布如图所见,经纬度位置与地球贴图位置对不上,那么需要进行位置调整。纬度跟canvas坐标一个问题,都是要取反才正确经度跟贴图位置偏差Math.PI*0.5lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + Math.PI * 0.5;
latHelper.rotation.x = THREE.MathUtils.degToRad(-lat);4.地球柱体的使用 var myEarth = new MyEarth();
window.myEarth = myEarth;
myEarth.initThree(document.getElementById('canvas'));
myEarth.createChart({
bg: '#000080',//背景色
borderColor: '#1E90FF',//描边颜色
blurColor: '#1E90FF',//边界模糊颜色
borderWidth: 1,//描边宽度
blurWidth: 5,//边界模糊范围
fillColor: 'rgb(30 ,144 ,255,0.3)',//区块填充颜色
barHueStart: 0.2,//开始色相
barHueEnd: 0.5,//结束色相
barLightStart: 0.5,//开始亮度
barLightEnd: 0.78//结束亮度
});5.优化大量柱体柱体数量太多会让THREE渲染有压力,可以采用以下两种常用方式进行优化1.mergeGeometries优化将所有柱体合并成一个整的形状,减少形状数量。计算每个柱体的形状: 因为柱体不再是一个单独的Mesh不能用scale来缩放,达到高度随数值变化,而是要在生成图形时把缩放高度也加到变化矩阵中。这时候需要在positionHelper位置辅助对象的基础上再加上一个originHelper缩放辅助对象。缩放辅助对象是针对柱体大小的,柱体原始大小是1,所以将变换位置往外偏移0.5,相对于地球表面缩放。 this.originHelper = new THREE.Object3D();
this.originHelper.position.z = 0.5;
this.positionHelper.add(this.originHelper);最终柱体的位置和大小 位置辅助对象根据数值缩放,缩放辅助对象跟着改变,得到最终柱体矩阵 lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + Math.PI * 0.5;
latHelper.rotation.x = THREE.MathUtils.degToRad(-lat);
positionHelper.updateWorldMatrix(true, false);
//位置辅助对象根据数值缩放
positionHelper.scale.set(0.01, 0.01, THREE.MathUtils.lerp(0.01, 0.5, amount));
//最终柱体矩阵
originHelper.updateWorldMatrix(true, false);
geometry.applyMatrix4(originHelper.matrixWorld);多个柱体合并成一个形状this.geometries=[]
//柱体颜色
const color = new THREE.Color();
color.setHSL(hue, saturation, lightness);
//柱体形状
const geometry = new THREE.BoxGeometry(1, 1, 1);
const rgb = color.toArray().map((v) => v * 255);
//颜色数组等于顶点数*3
const colors = new Uint8Array(3 * geometry.getAttribute('position').count);
// 将颜色赋值到每个顶点的颜色数组中
colors.forEach((v, ndx) => {
colors[ndx] = rgb[ndx % 3];
});
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
//....柱体的位置和大小
//添加到合并形状数组中
this.geometries.push(geometry);
//合并形状
const mergedGeometry = BufferGeometryUtils.mergeGeometries(this.geometries, false);
const boxmesh = new THREE.Mesh(
mergedGeometry,
new THREE.MeshBasicMaterial({
//颜色使用顶点颜色
vertexColors: true
})
);
this.scene.add(boxmesh);注意:BufferGeometryUtils.mergeGeometries方法在three的0.124.0版本不存在,在目前最新版本0.156.1存在,不同版本之间的图元Geometry的格式有所不同,即便复制高版本的BufferGeometryUtils到低版本也不能完全兼容,所以要注意three的版本2.InstancedMesh优化如果必须渲染大量具有相同几何体和材质但具有不同世界变换的对象,用InstancedMesh。 InstancedMesh适用于大量复用,它能减少绘制调用的次数,从而提高应用程序的整体渲染性能。InstancedMesh能被Raycaster监测到instanceId实例索引,进而可以设置交互 this.mesh = new THREE.InstancedMesh(boxGeometry, boxMaterial, res.length);
this.scene.add(this.mesh);
//设置柱体变换矩阵和颜色
res.forEach((a, i) => {
///....计算柱体颜色,位置大小
//设置实例颜色
this.mesh.setColorAt(i, color);
//设置实例变换矩阵
this.mesh.setMatrixAt(i, originHelper.matrixWorld);
});
//更新柱体网格颜色
this.mesh.instanceColor.needsUpdate = true;
//更新柱体网格位置
this.mesh.instanceMatrix.needsUpdate = true;6.地球出场动画实现效果:地球从小变大,并且边旋转,视角边转到对应位置,柱体从短慢慢往外延伸到对应高度this.objGroup.scale.set(0.1, 0.1, 0.1);
//开始视角
let orgCamera = this.camera.position;
let orgControl = this.controls.target;
const { cameraPos, controlPos } = this.that;
//视角,缩放,旋转的出场动画
let tween = new TWEEN.Tween({
scale: 0.1,
rotate: 0,
cameraX: orgCamera.x,
cameraY: orgCamera.y,
cameraZ: orgCamera.z,
controlsX: orgControl.x,
controlsY: orgControl.y,
controlsZ: orgControl.z
})
.to(
{
scale: 1,
rotate: Math.PI,
//目标视角
cameraX: cameraPos.x,
cameraY: cameraPos.y,
cameraZ: cameraPos.z,
controlsX: controlPos.x,
controlsY: controlPos.y,
controlsZ: controlPos.z
},
2000//持续时间
)
//时间变化方法
.easing(TWEEN.Easing.Quadratic.Out)
//动画更新
.onUpdate((obj) => {
this.objGroup.scale.set(obj.scale, obj.scale, obj.scale);
this.objGroup.rotation.y = obj.rotate;
this.camera.position.set(obj.cameraX, obj.cameraY, obj.cameraZ);
this.controls.target.set(obj.controlsX, obj.controlsY, obj.controlsZ);
})
//链式执行下一个动画
.chain(
//柱体动画
new TWEEN.Tween({ h: this.barMin })
.to({ h: this.barMax }, 2000)
.easing(TWEEN.Easing.Quadratic.Out)
.onUpdate((obj) => {//更新柱体高度
this.currentBarH = obj.h;
this.addBars(res);
})
)
.start();
TWEEN.add(tween);Githubhttps://github.com/xiaolidan00/my-earth参考:threejs.org/manual/#en/…
Ned
基于three.js的牛逼轰轰的3D编辑器nunuStudio!
这是一款基于Three.js的3D编辑器,我之前一直喊错,叫人家"牛牛",因为我觉得它真的好牛,其实人家正确拼音喊“努努”!官网地址:https://www.nunustudio.org/,里面内容很详细。我也是刚用不久,在这里写个入门教程。 推荐下载他的electron的版本的安装包,本地运行快点!并且可以发布web的运行包,直接可以网页端二次开发,真的不要太方便了!1.布局介绍 配置一下场景,然后点击上方的run就可以看到预览界面效果了2.工具栏介绍选中对象,当然也可以直接在右侧的对象列表里点击选中移动对象,对应position属性x,y,z,也可以在配置板面直接修改对应数值缩放对象,对应scale属性x,y,z,也可以在配置板面直接修改对应数值旋转对象,对应rotation属性的x,y,z,也可以在配置板面直接修改对应数值其实我感觉这个操作对象,分开一个个点有点鸡肋,很不习惯!1.2 对象栏添加形状物体,不同物体的配置板面属性有所不同添加字体添加光源,常用的环境光,平行光,点光源等都有,像点光源之类的默认是勾选了产生投影的添加镜头,有透视镜头和正交镜头,添加后可以看到镜头视角的预览画面,如果需要把这个镜头的画面作为运行画面,需要勾选use camera添加控制器,camera记得要勾选use camera3.添加脚本(运行时可用,编辑时效果不可见)可用参数● self 对象属性 (position, rotation, scale, children等).● program 当前程序包含当前场景scene,里面的资源resources,可通过(getMaterialByName, getTextureByName等方法获取)● scene运行场景.● Keyboard 键盘输入● Mouse鼠标操作函数● initialize在加载场景时调用,它通常用于以编程方式创建新对象、从场景中获取对象、初始化变量等.● update在将场景绘制到屏幕之前调用每个帧,它可以用于控制对象、获取输入值、更改对象属性等.● dispose当程序关闭时调用,应用于清理对象、断开与数据流的连接等.● onMouseOver(objects)当鼠标位于脚本对象的子对象上时调用,可以与鼠标函数组合检查对象是否被单击。接收到对象数组作为参数.● onResize(x, y) 每次调整程序窗口的大小时调用.● onAppData(data) 用于从主机页接收数据,数据作为参数接收,用于消息传递1.操作对象,绕脚本中心点旋转function initialize()
{
self.position.x += 2;
}
function update()
{
self.rotation.y += 0.01;
}2.鼠标键盘控制function update()
{
//鼠标控制旋转
self.rotation.y += Mouse.delta.x * 0.01;
//键盘控制移动
if(Keyboard.keyPressed(Keyboard.LEFT))
{
self.position.x -= 0.1;
}
if(Keyboard.keyPressed(Keyboard.RIGHT))
{
self.position.x += 0.1;
}
}3.获取场景中对象var object;
var count=0
function initialize()
{
object = scene.getObjectByName("box");
object.position.y=2;
}
function update()
{
//使用该材质的对象随机变色
if(count%60==0){
object.material.color=new THREE.Color(`rgb(${parseInt(Math.random()*200)},${parseInt(Math.random()*200)},${parseInt(Math.random()*200)})`);
}
count++;
}4.获取子对象var object1,object2;
function initialize()
{
object1 = self.children[0];//cylinder
object2 = self.children[1];//box
}
function update()
{
object1.rotation.x += 0.01;
object2.rotation.y += 0.01;
}
5.右击顶层program,新增一个scene场景,双击那个场景进行编辑,可通过脚本切换场景//在场景1添加脚本
function update()
{
//按w切换到场景2
if(Keyboard.keyPressed(Keyboard.W))
{//获取程序里的场景2
program.setScene("scene2");
}
}6.添加html,注意这个不是CSS2DRender,不会跟着场景元素进行变换var element = document.createElement("div");
element.style.width = "100px";
element.style.height = "100px";
element.style.position = "absolute";
element.style.top = "100px";
element.style.left = "100px";
element.style.backgroundColor = "#0000FF";
function initialize()
{
//添加html
program.division.appendChild(element);
}
function dispose()
{
//销毁时记得删除对应元素
program.division.removeChild(element);
}
7.引入外部脚本,先把js导入项目,再用include来引入库,进行使用include("moment.js");
var text;
function initialize()
{
text=program.getObjectByName('text');
console.log(moment)
}
function update(){
text.setText(moment().format('LTS'));
}
网页端和electron有所不同,网页端应该是最新的,有些奇怪的bug已经修复,比如在electron里面是脚本一旦运行错误,你就得重新打开nunuStudio才能再run,网页端就不会这样禁止运行3.材质与贴图可以创建需要的材质和贴图,双击对应的资源就可以进入配置界面贴图可以拖入材质的对应texture属性里面,也可以直接上传图片作为新的贴图材质可以通过拖拽赋值到对应的对象里面,鼠标悬浮对应材质,对应材质会变色,就知道这个对象用什么材质了。这个材质和贴图管理有点麻烦,添加贴图和材质莫名生成一堆重复的,一旦材质和贴图资源太多,就会导致很难辨别对象使用的是谁,而且你删除对象的时候,要是对应材质和贴图没有引用,它也不会主动删除,还是会留在资源库里,得手动删除。4.raycaster拾取对象先添加一个camera镜头作为射线拾取的视图,然后添加模型,根据点击设置对应的材质var red, blue;
function initialize()
{
red = program.getMaterialByName("red");
blue = program.getMaterialByName("blue");
}
function update()
{
//获取射线拾取到的对象
var intersects = scene.raycaster.intersectObjects(scene.children);
for(var i = 0; i < intersects.length; i++)
{//左点击设置为red的材质
if(Mouse.buttonPressed(Mouse.LEFT))
{
intersects[i].object.material = red;
}
//右点击设置为blue的材质
else if(Mouse.buttonPressed(Mouse.RIGHT))
{
intersects[i].object.material = blue;
}
}
}5.发布web网页和导出模型调整好在nunuStudio的效果,可以导出一个web网页,就是对应的预览界面了index.html里面的代码里有Nunu加载的代码,可进行二次开发,里面是直接可以用THREE的,但是控制器推荐用nunu的,估计它里面用改写过controls,自己添加的THREE的control会报错。里面有些bug,比如飞线之类的动态效果,在导出的预览文件无法复现效果,而在编辑器可以正常运行。我也不知道为什么!var app;
//Initialize app
function run()
{
//Create app object
app = new NunuApp();
//Onload enable the vr and fullscreen buttons
var logo = document.getElementById("logo");
//加载成功后
var onLoad = function()
{
var button = document.getElementById("fullscreen");
button.style.visibility = "visible";
//Check if VR is available
if(app.vrAvailable())
{
//If there are displays available add button
Nunu.getVRDisplays(function(display)
{
button = document.getElementById("vr");
button.style.visibility = "visible";
});
}
//Remove logo and loading bar
document.body.removeChild(logo);
};
//下载进度条
var bar = document.getElementById("bar");
var onProgress = function(event)
{
if(event.lengthComputable)
{
var progress = event.loaded / event.total * 100;
bar.style.width = progress + "%";
}
};
//Load and run nunu app
app.loadRunProgram("app.nsp", onLoad, onProgress);
}
//window resize时调整大小
function resize()
{
app.resize();
}
//全屏
function toggleFullscreen()
{
app.toggleFullscreen(document.body);
}
//Toggle VR mode
function toggleVR()
{
app.toggleVR();
}调好效果可以导出模型,这个也有点bug,有些格式不能完全导出所有的对象和效果,gltf格式,比如hemisphere光源没跟着导出来,材质里面的emissveMap,alphaMap也没跟着导出来,因此导入模型后要手动代码搞一下。总结我这里介绍的都是常用的,官网上还有许多优秀的案例,以及功能介绍,什么动画啊,物理啊,粒子,后期效果啊!还能做游戏!真的太多了!你可以在上面搞得差不多再导出来二次开发,省了很多效果设置的功夫! 总体来说,虽然有些bug和操作的鸡肋,但是真的是很优秀的一款3D编辑器!期待会越来越好!我基于官方的nunuStudio的github地址fork了一下,根据自己和建模师的使用习惯,修改了一些操作代码地址:https://github.com/xiaolidan00/nunuStudio/tree/dev-0.9.6
Ned
掌握JavaScript函数,让你的代码更加优雅
函数的概述什么是函数?将一个(反复)使用的功能,封装成一个独立的模块,这个模块叫做函数。2. 好处使程序可控 一次封装,无限次使用(增加代码的复用度)3. 函数的类型Function4. 函数的分类内置函数(库函数、系统函数) 自定义函数函数的声明语句定义法(声明式函数): 可以任意位置调用!function 函数名([参数]){
//功能
}2. 表达式定义法(赋值式函数):只能先声明,后使用!var 变量名 = function([参数]){
//功能
} <script>
//声明式函数
function fn1(){
// 输出1~100中的所有素数(素数:只能被1和它本身整除的数)
// 1~100 i = 1;i < 101;i ++
// j = 2;j <= 本身; j ++
var str = ''; //放置所有素数的变量
for(var i = 1;i < 101;i ++){
for(var j = 2;j <= i;j ++){
if(i % j === 0){
break;
}
}
if(i === j){
str += i + ' ';
}
}
document.write(str);
}
fn1();
document.write('<br>');
fn1();
document.write('<br>');
fn1();
</script>函数的调用一般调用 :函数名([参数])函数的参数形式参数(形参) :声明函数时使用的参数实际参数(实参) :调用函数时使用的参数 <script>
//声明函数
function fn(m,n){ //形参(形式参数):用于接收实参的值,必须是变量
// 输出m行n列的8
//m 行: 从第一行开始,最m行结束,步长
var str = '';
for(var row = 1;row <= m;row ++){
//n 列: 从第一列开始,最n列结束,步长
for(var col = 1;col <= n;col ++){
str += 8;
}
str += '\n';
}
console.log(str);
console.log(m,n);
}
// m = 8
var a = parseInt(prompt('请输入行数:'));
var b = parseInt(prompt('请输入列数:'));
fn(a,b); //实参(实际参数):给形参传递的数据,可以是常量 ,变量,表达式
</script>如果实参的数量少于形参的数量时,多余的形参值为undefined 如果实参的数量多于形参的数量时,多余的实参忽略。函数的返回值return将函数中处理后的结果返回到调用该函数的地方。退出函数2. 比如 1 + 2 是一个表达式,那么 这个表达式的结果就是 3console.log(1 + 2) // 3
function fn() {
// 执行代码
}
// fn() 也是一个表达式,这个表达式就没有结果出现
console.log(fn()) // undefined <script>
//返回值:将函数中处理的功能后,得到的结果返回到调用这个函数的地方。
function fn(){
// 输出一元钱的兑换方案(1角2角5角)
1角:var one = 0;one < 11;one ++
2角:var two = 0;two < 6;two ++
5角:var five = 0;five < 3;five ++
if(1 * one + 2 * two + 5 * five === 10){
}
var str = '';
for(var one = 0;one < 11;one ++){
for(var two = 0;two < 6;two ++){
for(var five = 0;five < 3;five ++){
if(1 * one + 2 * two + 5 * five === 10){
str += one + ' 张壹角 ' + two + ' 张贰角 ' + five + ' 张伍角\n';
}
}
}
}
console.log(str);
return str; //将处理后的结果返回到调用这个函数的地方。
退出函数了
alert('瞧瞧');
}
var result = fn(); //调用fn()的函数,将返回的结果str赋值给result变量
console.log(result);
console.log(fn());
fn();
console.log(fn());
</script>函数作用域JavaScript中有全局作用域和函数作用域两种作用域,函数作用域只在函数内部有效。在函数内定义的变量和函数只能在该函数内部访问,不能在函数外部访问。以下是函数作用域的示例:function greet(name) {
const message = 'Hello, ' + name + '!';
console.log(message);
}
greet('Alice'); // 输出 Hello, Alice!
console.log(message); // 抛出错误:message未定义闭包闭包是指在一个函数中定义了另一个函数,并且内部函数可以访问外部函数的变量。在JavaScript中,函数可以嵌套定义,内部函数可以访问外部函数的变量,从而形成闭包。以下是闭包的示例:function add(a) {
return function(b) {
return a + b;
}
}
const add2 = add(2);
console.log(add2(3)); // 输出 5在上面的示例中,add() 函数返回一个匿名函数,这个匿名函数可以访问 add() 函数的参数 a。定义 add2 变量时,将 add(2) 赋值给它,此时 add2 变量就成为了一个新的函数,这个函数可以接收一个参数并返回参数与 a 的和。回调函数回调函数是指将函数作为参数传递给另一个函数,并在另一个函数中调用它。在JavaScript中,回调函数常用于异步编程中,在异步操作完成后执行回调函数。以下是回调函数的示例:function add(a, b, callback) {
const result = a + b;
setTimeout(function() {
callback(result);
}, 1000);
}
add(2, 3, function(result) {
console.log(result); // 输出 5
});匿名函数除了通过function关键字定义函数外,还可以使用匿名函数,例如:var add = function(a, b) {
return a + b;
};
var result = add(3, 4); // 结果为7函数作为参数在JavaScript中,函数可以作为参数传递给其他函数,例如:function operate(a, b, operation) {
return operation(a, b);
}
var sum = operate(5, 3, function(x, y) { return x + y; }); // 结果为8案例使用函数求三个数中的最大数 <script>
//使用函数求三个数中的最大数
比较 - 选择语句
1. 先求出前两个数中的最大值 一个条件,两个结果(双分支语句)
2. 再拿前两个数中的最大值与剩下的下数进行比较,一个条件,一个结果(单分支)
//求三个数中最大值的函数
function fnMax(a,b,c){
//求最大值
var max = a > b ? a : b;
//与剩下的一个数进行比较
if(c > max){
max = c;
}
return max;
}
// alert(fnMax(4,2,5));
编写函数iseven,其功能为判断一个整数是否为偶数,若是偶数,返回1,否则返回0,在程序中调用此函数,对输入的一个整数进行判断,若是偶数,输出even,否则输出odd
//封装一个判断奇偶的函数,将结果返回
function iseven(n){
return n % 2 ? 0 : 1;
}
// 输入的一个整数
var i = parseInt(prompt('请输入一个整数:'));
//声明一个变量,接收函数的返回值
var result = iseven(i);
//根据返回值,判断输出even还是odd
alert(result === 1 ? 'even' : 'odd');
</script>编程求x的阶乘和y的阶乘的和,要求设计一个fac(n)函数求正整数n的阶乘 <script>
编程求x的阶乘和y的阶乘的和,要求设计一个fac(n)函数求正整数n的阶乘
x的阶乘
y的阶乘
和
//封装一个求正整数n的阶乘
function fac(n){
for(var i = 1,result = 1;i <= n;i ++){
result *= i;
}
return result;
}
//准备两个变量,接收两个整数
var x = parseInt(prompt('请输入一个整数:'));
var y = parseInt(prompt('请输入一个整数:'));
//求和
var sum = fac(x) + fac(y);
alert(sum);
</script>利用函数实现面积计算器(计算长方形、三角形、圆形的面积)。Math.PI <script>
利用函数实现面积计算器(计算长方形、三角形、圆形的面积)。Math.PI
1. 长方形: 长 * 宽
2. 三角形:底 * 高 / 2
3. 圆形: Math.PI * r * r
//封装一个长方形面积的函数
function rectangle(long,width){
return long * width;
}
//封装一个三角形面积的函数
function triangle(bottom,height){
return (bottom * height / 2).toFixed(2);
}
//封装一个圆形面积的函数
function circle(r){
return (Math.PI * r * r).toFixed(2);
}
//封装一个计算器的函数
function calculator(){
//设计一个无限循环
while(1){
//提示用户输入功能
var n = parseInt(prompt('0: 退出 1: 长方形 2: 三角形 3: 圆形\n请选择:'));
//判断
switch(n){
case 1 :
//输入长
var long = parseInt(prompt('请输入长方形的长:'));
//输入宽
var width = parseInt(prompt('请输入长方形的宽:'));
alert('长方形的面积是:' + rectangle(long,width));
break;
case 2 :
//输入底
var bottom = parseInt(prompt('请输入三角形的底:'));
var height = parseInt(prompt('请输入三角形的高:'));
alert('三角形的面积是:' + triangle(bottom,height));
break;
case 3 :
//输入半径
var r = parseInt(prompt('请输入圆形的半径:'));
alert('圆形的面积是:' + circle(r));
break;
case 0 : return; //退出函数
default : alert('其它图形的面积尚未开放,敬请期待!');
}
}
}
calculator();
</script>计算s=2^2!+3^2! <script>
计算s=2^2!+3^2!
2^2 求2的平方 2 * 2
阶乘
和
//1. 求平方函数
function square(n){
return n * n;
}
//2. 阶乘
function fac(n){
for(var i = 1,result = 1;i <= n;i ++){
result *= i;
}
return result;
}
//3. 和
function sum(){
return fac(square(2)) + fac(square(3));
}
alert(sum());
</script>递归函数自己调用自己的过程。本质:循环三要素:从哪里开始,到哪里结束、步长建议在循环嵌套不确定层数时,使用递归函数。// 下面这个代码就是一个最简单的递归函数
// 在函数内部调用了自己,函数一执行,就调用自己一次,在调用再执行,循环往复,没有止尽
function fn() {
fn()
}
fn()案例1 + 2 + 3 + …… + 100的和 <script>
//递归:实现循环 (三要素)
//求 1 + 2 + 3 + …… + n 的和
function fnSum(n){ //从哪里开始 5 4 3 2 1
//到哪里结束,退出循环的条件
if( n === 1){
return 1;
}else{
return n + fnSum(n - 1); //步长
// 5 + 4 + 3 + 2 + 1
}
}
console.log(fnSum(5));
// 求 n!
function fnFac(n){
if(n === 1){
return 1;
}else {
return n * fnFac(n - 1);
}
}
console.log(fnFac(5));
</script>斐波那契数列 <script>
//斐波那契数列: 下一个数是前两个数的和
// 1 1 2 3 5 8 13 21 34 55
function fib(n){ //循环初值,代表的是第几个斐波那契数字
//循环条件(退出条件)
if(n == 1 || n == 2){
return 1;
}else{
return fib(n - 1) + fib(n - 2);
}
}
// alert(fib(10));
for(var i = 1;i < 21;i ++){
console.log(fib(i));
}
</script>输出10个Hello world <script>
//输出10个Hello world
function print(n){
if(n === 0){
return;
}else{
console.log('helloworld');
print(n - 1);
}
}
print(5);
</script>作用域及作用域链详解 <script>
//1. 作用域:JS代码作用的范围
//2. 一旦进入作用域,浏览器就会启动JS解析器
一、寻找东西? (var function 形参)
a = 1
fn = function fn(){alert(2)}
//1. 当找到var 或者 形参 时,取后面的名字存储在内存中,并给它初始化一个值undefined
//2. 当找到function时,取函数名存储在内存中,并将整个函数块赋值给这个函数名.
二、逐行解读代码 (遇到函数声明,则直接跳过)
1. 执行表达式
2. 函数调用
alert(a); //undefined
var a = 1;
alert(a); //1
function fn(){
alert(2);
}
alert(a); //1
</script> <script>
//1. 作用域:JS代码作用的范围
//2. 一旦进入作用域,浏览器就会启动JS解析器
一、预解析(寻找东西?) (var function 形参)
a = 3
//1. 当找到var 或者 形参 时,取后面的名字存储在内存中,并给它初始化一个值undefined
//2. 当找到function时,取函数名存储在内存中,并将整个函数块赋值给这个函数名.
//3. 当变量名与函数名相同时,丢变量、保函数
二、逐行解读代码 (遇到函数声明,则直接跳过)
1. 执行表达式
2. 函数调用
alert(a); //function a(){alert(4)}
var a = 1;
alert(a); //1
function a(){
alert(2);
}
alert(a); //1
var a = 3;
alert(a); //3
function a(){
alert(4);
}
alert(a); //3
</script> <script>
//1. 作用域:JS代码作用的范围
//2. 一旦进入作用域,浏览器就会启动JS解析器
一、预解析(寻找东西?) (var function 形参)
fn = function fn(){alert(2)}
a = 1
//1. 当找到var 或者 形参 时,取后面的名字存储在内存中,并给它初始化一个值undefined
//2. 当找到function时,取函数名存储在内存中,并将整个函数块赋值给这个函数名.
//3. 当变量名与函数名相同时,丢变量、保函数
//4. 当有多个script标签时,从上到下依次解决每一个script标签,所以建议大家 《将所有声明的语句放到第一个script标签中》
二、逐行解读代码 (遇到函数声明,则直接跳过)
1. 执行表达式
2. 函数调用
alert(a); //报错
function fn(){
alert(2);
}
</script>
<script>
var a = 1;
fn(); //2
</script> <script>
//1. 作用域:JS代码作用的范围
//2. 一旦进入作用域,浏览器就会启动JS解析器(一、预 解析 二、逐行解读代码)
//3. var 声明在script作用域的变量,称为全局变量,作用范围是整个页面,生存周期在页面开始运行时开启空间,到页面退出时释放内存空间。同时也是window对象的属性。如果var声明的变量或形参在函数中时,称为局部变量,作用范围仅限于当前函数中,生存周期在函数调用时开启空间,到函数调用结束后,释放内存空间。
//4. 作用域链:从一个作用域中未寻找到内容,会向父级作用域中查找,这个向上查找的过程,称为作用域链。
一、预解析(寻找东西?) (var function 形参)
a = 2
fn = function(){}
//1. 当找到var 或者 形参 时,取后面的名字存储在内存中,并给它初始化一个值undefined
//2. 当找到function时,取函数名存储在内存中,并将整个函数块赋值给这个函数名.
//3. 当变量名与函数名相同时,丢变量、保函数
//4. 当有多个script标签时,从上到下依次解决每一个script标签,所以建议大家 《将所有声明的语句放到第一个script标签中》
二、逐行解读代码 (遇到函数声明,则直接跳过)
1. 执行表达式
2. 函数调用 (函数也是一个作用域)
一、预解析
二、逐行解读代码
1. 执行表达式
2. 函数调用
alert(a); //undefined
var a = 1;
alert(a); //1
function fn(){
alert(a); //1
a = 2;
alert(a); //2
}
fn();
alert(a); //2
</script> <script>
//1. 作用域:JS代码作用的范围
//2. 一旦进入作用域,浏览器就会启动JS解析器(一、预 解析 二、逐行解读代码)
//3. var 声明在script作用域的变量,称为全局变量,作用范围是整个页面,生存周期在页面开始运行时开启空间,到页面退出时释放内存空间。同时也是window对象的属性。如果var声明的变量或形参在函数中时,称为局部变量,作用范围仅限于当前函数中,生存周期在函数调用时开启空间,到函数调用结束后,释放内存空间。
//4. 作用域链:从一个作用域中未寻找到内容,会向父级作用域中查找,这个向上查找的过程,称为作用域链。
一、预解析(寻找东西?) (var function 形参)
a = 1
fn = function(){}
//1. 当找到var 或者 形参 时,取后面的名字存储在内存中,并给它初始化一个值undefined
//2. 当找到function时,取函数名存储在内存中,并将整个函数块赋值给这个函数名.
//3. 当变量名与函数名相同时,丢变量、保函数
//4. 当有多个script标签时,从上到下依次解决每一个script标签,所以建议大家 《将所有声明的语句放到第一个script标签中》
二、逐行解读代码 (遇到函数声明,则直接跳过)
1. 执行表达式
2. 函数调用 (函数也是一个作用域)
一、预解析
a = 2
二、逐行解读代码
1. 执行表达式
2. 函数调用
alert(a); //undefined
var a = 1;
alert(a); //1
function fn(){
alert(a); //undefined
var a = 2;
alert(a); //2
}
fn();
alert(a); //1
</script> <script>
//1. 作用域:JS代码作用的范围
//2. 一旦进入作用域,浏览器就会启动JS解析器(一、预 解析 二、逐行解读代码)
//3. var 声明在script作用域的变量,称为全局变量,作用范围是整个页面,生存周期在页面开始运行时开启空间,到页面退出时释放内存空间。同时也是window对象的属性。如果var声明的变量或形参在函数中时,称为局部变量,作用范围仅限于当前函数中,生存周期在函数调用时开启空间,到函数调用结束后,释放内存空间。
//4. 作用域链:从一个作用域中未寻找到内容,会向父级作用域中查找,这个向上查找的过程,称为作用域链。
一、预解析(寻找东西?) (var function 形参)
a = 1
fn = function(){}
//1. 当找到var 或者 形参 时,取后面的名字存储在内存中,并给它初始化一个值undefined
//2. 当找到function时,取函数名存储在内存中,并将整个函数块赋值给这个函数名.
//3. 当变量名与函数名相同时,丢变量、保函数
//4. 当有多个script标签时,从上到下依次解决每一个script标签,所以建议大家 《将所有声明的语句放到第一个script标签中》
二、逐行解读代码 (遇到函数声明,则直接跳过)
1. 执行表达式
2. 函数调用 (函数也是一个作用域)
一、预解析
a = 2
二、逐行解读代码
1. 执行表达式
2. 函数调用
var a = 1;
alert(a); //1
function fn(a){ //形参
alert(a); //undefined
a = 2;
alert(a); //2
}
fn();
alert(a); //1
</script> <script>
//1. 作用域:JS代码作用的范围
//2. 一旦进入作用域,浏览器就会启动JS解析器(一、预 解析 二、逐行解读代码)
//3. var 声明在script作用域的变量,称为全局变量,作用范围是整个页面,生存周期在页面开始运行时开启空间,到页面退出时释放内存空间。同时也是window对象的属性。如果var声明的变量或形参在函数中时,称为局部变量,作用范围仅限于当前函数中,生存周期在函数调用时开启空间,到函数调用结束后,释放内存空间。
//4. 作用域链:从一个作用域中未寻找到内容,会向父级作用域中查找,这个向上查找的过程,称为作用域链。
一、预解析(寻找东西?) (var function 形参)
a = 1
fn = function(){}
//1. 当找到var 或者 形参 时,取后面的名字存储在内存中,并给它初始化一个值undefined
//2. 当找到function时,取函数名存储在内存中,并将整个函数块赋值给这个函数名.
//3. 当变量名与函数名相同时,丢变量、保函数
//4. 当有多个script标签时,从上到下依次解决每一个script标签,所以建议大家 《将所有声明的语句放到第一个script标签中》
二、逐行解读代码 (遇到函数声明,则直接跳过)
1. 执行表达式
2. 函数调用 (函数也是一个作用域)
一、预解析
a = 2
二、逐行解读代码
1. 执行表达式
2. 函数调用
var a = 1;
alert(a); //1
function fn(a){ //形参 a = 1
alert(a); //1
a = 2;
alert(a); //2
}
fn(a); //实参
alert(a); //1
</script> <script>
//1. 作用域:JS代码作用的范围
//2. 一旦进入作用域,浏览器就会启动JS解析器(一、预 解析 二、逐行解读代码)
//3. var 声明在script作用域的变量,称为全局变量,作用范围是整个页面,生存周期在页面开始运行时开启空间,到页面退出时释放内存空间。同时也是window对象的属性。如果var声明的变量或形参在函数中时,称为局部变量,作用范围仅限于当前函数中,生存周期在函数调用时开启空间,到函数调用结束后,释放内存空间。
//4. 作用域链:从一个作用域中未寻找到内容,会向父级作用域中查找,这个向上查找的过程,称为作用域链。
一、预解析(寻找东西?) (var function 形参)
a = 2
fn = function(){}
//1. 当找到var 或者 形参 时,取后面的名字存储在内存中,并给它初始化一个值undefined
//2. 当找到function时,取函数名存储在内存中,并将整个函数块赋值给这个函数名.
//3. 当变量名与函数名相同时,丢变量、保函数
//4. 当有多个script标签时,从上到下依次解决每一个script标签,所以建议大家 《将所有声明的语句放到第一个script标签中》
//5. 如果在所有的作用域中都没有找到这个变量,则会在全局作用域中因为赋值号的原因,自动生成这个变量。
二、逐行解读代码 (遇到函数声明,则直接跳过)
1. 执行表达式
2. 函数调用 (函数也是一个作用域)
一、预解析
二、逐行解读代码
1. 执行表达式
2. 函数调用
function fn(){
a = 2;
alert(a); //2
}
fn();
alert(a); //2
</script>
Ned
开饭啦!恰个3D饼图
用Three.js搞个3D饼图1.准备工作(1)渐变颜色/**
* 获取暗色向渐变颜色
* @param {string} color 基础颜色
* @param {number} step 数量
* @returns {array} list 颜色数组
*/
export function getShadowColor(color, step) {
let c = getColor(color);
let { red, blue, green } = c;
const s = 0.8;
const r = parseInt(red * s),
g = parseInt(green * s),
b = parseInt(blue * s);
//获取亮色向渐变颜色
// const l = 0.2;
//const r = red + parseInt((255 - red) * l),
// g = green + parseInt((255 - green) * l),
// b = blue + parseInt((255 - blue) * l);
const rr = (r - red) / step,
gg = (g - green) / step,
bb = (b - blue) / step;
let list = [];
for (let i = 0; i < step; i++) {
list.push(
`rgb(${parseInt(red + i * rr)},${parseInt(green + i * gg)},${parseInt(blue + i * bb)})`
);
}
return list;
}(2)生成文本的canvas精灵贴图生成文本canvas/**
* 生成文本canvas
* @param {array} textList [{text:文本,color:文本颜色}]
* @param {number} fontSize 字体大小
* @returns
*/
export function getCanvasTextArray(textList, fontSize) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = fontSize + 'px Arial';
//计算canvas宽度
let textLen = 0;
textList.forEach((item) => {
let w = ctx.measureText(item.text + '').width;
if (w > textLen) {
textLen = w;
}
});
canvas.width = textLen;
canvas.height = fontSize * 1.2 * textList.length;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = fontSize + 'px Arial';
//行高是1.2倍
textList.forEach((item, idx) => {
ctx.fillStyle = item.color;
ctx.fillText(item.text, 0, fontSize * 1.2 * idx + fontSize);
});
return canvas;
}生成文本精灵材质
/**
*生成文本精灵材质
* @param {THREE.js} THREE
* @param {array} textlist 文本颜色数组
* @param {number} fontSize 字体大小
* @returns
*/
export function getTextArraySprite(THREE, textlist, fontSize) {
//生成五倍大小的canvas贴图,避免大小问题出现显示模糊
const canvas = getCanvasTextArray(textlist, fontSize * 5);
const map = new THREE.CanvasTexture(canvas);
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
const material = new THREE.SpriteMaterial({
map: map,
depthTest: false,
side: THREE.DoubleSide
});
const mesh = new THREE.Sprite(material);
//缩小等比缩小canvas精灵贴图
mesh.scale.set(canvas.width * 0.1, canvas.height * 0.1);
return { mesh, material, canvas };
}注意:canvas贴图一定要是原始大小的几倍,否则会因为贴图大小导致显示模糊的情况(3)扇形柱体弧度与角度计算360度是全部的值加起来的结果,那么每项的值占的角度为 let angel = (item.value / sum) * Math.PI * 2同理,值与高度映射,基础高度加上映射高度 let allHeight = maxHeight - baseHeight;
let h = baseHeight + ((item.value - min) / valLen) * allHeight;2.画大饼createChart(that) {
this.that = that;
if (this.group) {
this.cleanObj(this.group);
this.group = null;
}
if (that.data.length == 0) {
return;
}
this.cLen = 3;
//从小到大排序
that.data = that.data.sort((a, b) => a.value - b.value);
//获取渐变色
this.colors = getDrawColors(that.colors, this.cLen);
let { baseHeight, radius, perHeight, maxHeight, fontColor, fontSize } = that;
let sum = 0;
let min = Number.MAX_SAFE_INTEGER;
let max = 0;
for (let i = 0; i < that.data.length; i++) {
let item = that.data[i];
sum += item.value;
if (min > item.value) {
min = item.value;
}
if (max < item.value) {
max = item.value;
}
}
let startRadius = 0;
let valLen = max - min;
let allHeight = maxHeight - baseHeight;
let axis = new THREE.Vector3(1, 0, 0);
let group = new THREE.Group();
this.group = group;
this.scene.add(group);
for (let idx = 0; idx < that.data.length; idx++) {
let objGroup = new THREE.Group();
objGroup.name = 'group' + idx;
let item = that.data[idx];
//角度范围
let angel = (item.value / sum) * Math.PI * 2;
//高度与值的映射
let h = baseHeight + ((item.value - min) / valLen) * allHeight;
//每个3D组成块组成:扇形柱体加两片矩形面
if (item.value) {
//创建渐变色材质组
let cs = this.colors[idx % this.colors.length];
let geometry = new THREE.CylinderGeometry(
radius,
radius,
h,
24,
24,
false,
startRadius, //开始角度
angel //扇形角度占有范围
);
let ms = [];
for (let k = 0; k < this.cLen - 1; k++) {
ms.push(getBasicMaterial(THREE, cs[k]));
}
//给不同面的设定对应的材质索引
geometry.faces.forEach((f, fIdx) => {
if (f.normal.y == 0) {
//上面和底面
geometry.faces[fIdx].materialIndex = 0;
} else {
//侧面
geometry.faces[fIdx].materialIndex = 1;
}
});
//扇形圆柱
let mesh = new THREE.Mesh(geometry, ms);
mesh.position.y = h * 0.5;
mesh.name = 'p' + idx;
objGroup.add(mesh);
const g = new THREE.PlaneGeometry(radius, h);
let m = getBasicMaterial(THREE, cs[this.cLen - 1]);
//注意图形开始角度和常用的旋转角度差90度
//封口矩形1
let r1 = startRadius + Math.PI * 0.5;
const plane = new THREE.Mesh(g, m);
plane.position.y = h * 0.5;
plane.position.x = 0;
plane.position.z = 0;
plane.name = 'c' + idx;
plane.rotation.y = r1;
plane.translateOnAxis(axis, -radius * 0.5);
objGroup.add(plane);
//封口矩形2
let r2 = startRadius + angel + Math.PI * 0.5;
const plane1 = new THREE.Mesh(g, m);
plane1.position.y = h * 0.5;
plane1.position.x = 0;
plane1.position.z = 0;
plane1.name = 'b' + idx;
plane1.rotation.y = r2;
plane1.translateOnAxis(axis, -radius * 0.5);
objGroup.add(plane1);
//显示label
if (that.isLabel) {
let textList = [
{ text: item.name, color: fontColor },
{ text: item.value + that.suffix, color: fontColor }
];
const { mesh: textMesh } = getTextArraySprite(THREE, textList, fontSize);
textMesh.name = 'f' + idx;
//y轴位置
textMesh.position.y = maxHeight + baseHeight;
//x,y轴位置
let r = startRadius + angel * 0.5 + Math.PI * 0.5;
textMesh.position.x = -Math.cos(r) * radius;
textMesh.position.z = Math.sin(r) * radius;
//是否开启动画
if (this.that.isAnimate) {
if (idx == 0) {
textMesh.visible = true;
} else {
textMesh.visible = false;
}
}
objGroup.add(textMesh);
}
group.add(objGroup);
}
startRadius = angel + startRadius;
}
//图形居中,视角设置
this.setModelCenter(group, that.viewControl);
}
animateAction() {
if (this.that?.isAnimate && this.group) {
this.time++;
this.rotateAngle += 0.01;
//物体自旋转
this.group.rotation.y = this.rotateAngle;
//标签显隐切换
if (this.time > 90) {
if (this.currentTextMesh) {
this.currentTextMesh.visible = false;
}
let textMesh = this.scene.getObjectByName('f' + (this.count % this.that.data.length));
textMesh.visible = true;
this.currentTextMesh = textMesh;
this.count++;
this.time = 0;
}
if (this.rotateAngle > Math.PI * 2) {
this.rotateAngle = 0;
}
}
}
}注意:(1)图形开始角度和常用的旋转角度差90度(2)这里使用了多材质,扇形柱体存在多个面,需要给不同面的设定对应的材质索引,这里直接根据法线y值判断geometry.faces.forEach((f, fIdx) => {
if (f.normal.y == 0) {
//上面和底面
geometry.faces[fIdx].materialIndex = 0;
} else {
//侧面
geometry.faces[fIdx].materialIndex = 1;
}
});(3)这里不使用FontLoader来加载生成字体mesh,因为如果要涵盖所有字,字体资源包贼大,为了渲染几个字加载那么大的东西,不值得!这里文本采用的canvas生成贴图,创建精灵材质,轻量普适性高!(4)我这里没有采用光照实现不同面的颜色偏差,而是采用渐变色,因为光照会导致颜色偏差较大,呈现出的颜色有可能不符合UI设计的效果。3.使用3D饼图 let cakeChart = new Cake();
cakeChart.initThree(document.getElementById('cake'));
cakeChart.createChart({
//颜色
colors: ['#fcc02a', '#f16b91', '#187bac'],
//数据
data: [
{ name: '小学', value: 100 },
{ name: '中学', value: 200 },
{ name: '大学', value: 300 }
],
//是否显示标签
isLabel: true,
//最大高度
maxHeight: 20,
//基础高度
baseHeight: 10,
//半径
radius: 30,
//单位后缀
suffix: '',
//字体大小
fontSize: 10,
//字体颜色
fontColor: 'rgba(255,255,255,1)',
//开启动画
isAnimate: true,
//视角控制
viewControl: {
autoCamera: true,
width: 1,
height: 1.6,
depth: 1,
centerX: 1,
centerY: 1,
centerZ: 1
}
});
//缩放时调整大小
window.onresize = () => {
cakeChart.onResize();
};
//离开时情况资源
window.onunload = () => {
cakeChart.cleanAll();
};开启动画不开启动画时4.环状3D饼图拆分成六个面,内外圈环面,上下环面,两个侧面 //外圈
let geometry = new THREE.CylinderGeometry(
outerRadius,
outerRadius,
h,
24,
24,
true,
startRadius,
angel
);
let mesh = new THREE.Mesh(geometry, getBasicMaterial(THREE, cs[1]));
mesh.position.y = h * 0.5;
mesh.name = 'p' + idx;
objGroup.add(mesh);
//内圈
let geometry1 = new THREE.CylinderGeometry(
innerRadius,
innerRadius,
h,
24,
24,
true,
startRadius,
angel
);
let mesh1 = new THREE.Mesh(geometry1, getBasicMaterial(THREE, cs[2]));
mesh1.position.y = h * 0.5;
mesh1.name = 'pp' + idx;
objGroup.add(mesh1);
let geometry2 = new THREE.RingGeometry(
innerRadius,
outerRadius,
32,
1,
startRadius,
angel
);
//上盖
let mesh2 = new THREE.Mesh(geometry2, getBasicMaterial(THREE, cs[0]));
mesh2.name = 'up' + idx;
mesh2.rotateX(-0.5 * Math.PI);
mesh2.rotateZ(-0.5 * Math.PI);
mesh2.position.y = h;
objGroup.add(mesh2);
//下盖
let mesh3 = new THREE.Mesh(geometry2, getBasicMaterial(THREE, cs[3]));
mesh3.name = 'down' + idx;
mesh3.rotateX(-0.5 * Math.PI);
mesh3.rotateZ(-0.5 * Math.PI);
mesh3.position.y = 0;
objGroup.add(mesh3);
let m = getBasicMaterial(THREE, cs[4]);
//侧面1
const g = new THREE.PlaneGeometry(ra, h);
const plane = new THREE.Mesh(g, m);
plane.position.y = h * 0.5;
plane.position.x = 0;
plane.position.z = 0;
plane.name = 'c' + idx;
plane.rotation.y = startRadius + Math.PI * 0.5;
plane.translateOnAxis(axis, -(innerRadius + 0.5 * ra));
objGroup.add(plane);
//侧面2
const plane1 = new THREE.Mesh(g, m);
plane1.position.y = h * 0.5;
plane1.position.x = 0;
plane1.position.z = 0;
plane1.name = 'b' + idx;
plane1.rotation.y = startRadius + angel + Math.PI * 0.5;
plane1.translateOnAxis(axis, -(innerRadius + 0.5 * ra));
objGroup.add(plane1);使用3D环状饼图 let cakeChart = new Cake();
cakeChart.initThree(document.getElementById('cake'));
cakeChart.createChart({
//颜色
colors: ['#fcc02a', '#f16b91', '#187bac'],
//数据
data: [
{ name: '小学', value: 100 },
{ name: '中学', value: 200 },
{ name: '大学', value: 300 }
],
//是否显示标签
isLabel: true,
//最大高度
maxHeight: 20,
//基础高度
baseHeight: 10,
//外半径
outerRadius: 30,
//内半径
innerRadius: 15,
//单位后缀
suffix: '',
//字体大小
fontSize: 10,
//字体颜色
fontColor: 'rgba(255,255,255,1)',
//开启动画
isAnimate: false,
//视角控制
viewControl: {
autoCamera: true,
width: 1,
height: 1.6,
depth: 1,
centerX: 1,
centerY: 1,
centerZ: 1
}
});注意环状饼图是要设置内外半径的文本标签位置=innerRadius + 0.5 * (outerRadius-innerRadius);Github地址https://github.com/xiaolidan00/my-three
Ned
探索JavaScript BOM:了解浏览器的内部机制和强大的API
BOM的概念和常用APIBOMBOM(Browser Object Model): 浏览器对象模型其实就是操作浏览器的一些能力我们可以操作哪些内容获取一些浏览器的相关信息(窗口的大小)操作浏览器进行页面跳转获取当前浏览器地址栏的信息操作浏览器的滚动条浏览器的信息(浏览器的版本)让浏览器出现一个弹出框(alert / confirm / prompt)...BOM 的核心就是 window 对象window 是浏览器内置的一个对象,里面包含着操作浏览器的方法innerHeight / innerWidth : 获取浏览器窗口的尺寸window.innerHeight: 表示窗口内容区域(浏览器窗口)的高度,包含滚动条的。window.innerWidth: 表示窗口内容区域(浏览器窗口)的宽度,包含滚动条的。var windowHeight = window.innerHeight
console.log(windowHeight)
var windowWidth = window.innerWidth
console.log(windowWidth)onload / onresize / onscroll 事件onload : 加载事件是在页面所有资源加载完毕后执行的window.onload = function () {
console.log('页面已经加载完毕')
}onscroll : 滚动事件这个 onscroll 事件是当浏览器的滚动条滚动的时候触发或者鼠标滚轮滚动的时候触发注: 必须有滚动条时,才可触发window.onscroll = function () {
console.log('浏览器滚动了')
}浏览器滚动的距离浏览器内的内容即然可以滚动,那么我们就可以获取到浏览器滚动的距离思考一个问题?浏览器真的滚动了吗?其实我们的浏览器是没有滚动的,是一直在那里滚动的是什么?是我们的页面所以说,其实浏览器没有动,只不过是页面向上走了所以,这个已经不能单纯的算是浏览器的内容了,而是我们页面的内容所以不是在用 window 对象了,而是使用 document 对象浏览器卷去的尺寸 scrollTop / scrollLeftscrollTop获取的是页面向上滚动的距离一共有两个获取方式document.body.scrollTopdocument.documentElement.scrollTopwindow.onscroll = function () {
console.log(document.body.scrollTop)
console.log(document.documentElement.scrollTop)
}两个都是获取页面向上滚动的距离区别:IE 浏览器没有 DOCTYPE 声明的时候,用这两个都行有 DOCTYPE 声明的时候,只能用 document.documentElement.scrollTopChrome 和 FireFox没有 DOCTYPE 声明的时候,用 document.body.scrollTop有 DOCTYPE 声明的时候,用 document.documentElement.scrollTopscrollLeft获取页面向左滚动的距离也是两个方法document.body.scrollLeftdocument.documentElementLeftwindow.onscroll = function () {
console.log(document.body.scrollLeft)
console.log(document.documentElement.scrollLeft)
}两个之间的区别和之前的 scrollTop 一样alert / confirm / prompt 弹出层alert() : 在浏览器中弹出一个警告框。confirm() : 在浏览器中弹出一个选择框。prompt() : 在浏览器中弹出一个输入框。练习(面试题)1.在界面上弹出一个带输入的提示框,只能输入数字,如果输入的数字大于10,点击确定以后,弹出提示框,提示我们是否进行输入的数 + 10, confirm("是否进行输入的数 + 10")<1>如果点击确定,输出表达式和结果,<2>如果点击取消,重新弹出带输入的提示框,重复上述操作如果输入的数小于10,直接弹出警告框,无法运算点击取消,就不进行后面操作。直接退出循环location / historylocation 对象页面跳转1.window.location2.location.href3.location.assign()刷新页面1.location.reload([true]) true: 不经过浏览器缓存,从远程服务器加载数据。 false(默认值):从本地缓存中加载数据。2.history 对象1.页面刷新 : history.go(0)2.后退: history.back()3.前进:history.forword()window.scrollTo()语法:window.scrollTo(x,y)window.scrollTo(options)2. 参数:x : 文档中的横轴坐标y : 文档中的纵轴坐标options : 一个包含三个属性的对象:1.top : 等同于 y2.left : 等同于 x3.behavior 类型 String,表示滚动行为,支持参数 smooth(平滑滚动),instant(瞬间滚动),默认值 autowindow.open / window.closeopen() : 打开新窗口close : 关闭当前窗口本地存储 localStorage / sessionStorage的基本使用localStorage : 本地存储理论上没有大小限制,实际上浏览器限制大小在5M数量没有限制有效期,长期有效不可在浏览器的隐私模式下查看,安全sessionStorage : 会话存储理论上没有大小限制,实际上浏览器限制大小在5M数量没有限制会话结束时,即关闭浏览器(退出页面)时,数据消失不可在浏览器的隐私模式下查看,安全如何在本地存储或会话存储中添加数据?window.localStorage.key = valuewindow.sessionStorage.key = valuewindow.localStorage['key'] = valuewindow.sessionStorage['key'] = valuewindow.localStorage.setItem(key,value)window.sessionStorage.setItem(key,value)如何获取本地存储或会话存储中的数据?window.localStorage.keywindow.sessionStorage.keywindow.localStorage['key']window.sessionStorage['key']window.localStorage.getItem(key)window.sessionStorage.getItme(key)如何删除本地存储或会话存储中的数据?window.localStorage.removeItem(key)window.sessionStorage.removeItem(key)清空window.localStorage.clear()window.sessionStorage.clear()获取所有的信息key() : 获取keylength : 长度for(let i = 0,len = window.localStorage.length;i <len ;i ++){
let key = window.localStorage.key(i); //获取所有的key
let value = window.localStorage.getItem(key); //获取所有的value
}DOM的基本概念及操作获取元素document.getElementById('id') : 通过id获取元素对象document.getElementsByTagName('标签名') : 通过标签名获取元素对象的集合,返回伪数组。document.getElementsByName('name属性') : 通过name属性获取元素对象的集合,返回伪数组。document.getElementsByClassName('class名') : 通过class属性获取元素对象的集合,返回伪数组 (IE9以下不兼容)//byClassName的兼容
function byClassName(obj,className){
if(obj.getElementsByClassName){
//支持
return obj.getElementsByClassName(className);
}else{
//不支持
//1. 获取所有节点对象
var eles = obj.getElementsByTagName('*');
//创建一个空数组
var arr = [];
//遍历所有对象
for(var i = 0,len = eles.length;i < len;i ++){
//检测
if(eles[i].className === className){
arr.push(eles[i]);
}
}
return arr;
}
}document.querySelector('选择器') : 通过选择器获取元素对象。 (IE8以下不兼容)document.querySelectorAll('选择器') : 获取选择器获取元素对象的集合,返回伪数组。(IE8以下不兼容)操作案例回到顶部<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body{
height: 5000px;
}
#box{
width: 50px;
height: 50px;
background: red;
color: yellow;
position: fixed;
right: 50px;
bottom: 50px;
display: none;
}
</style>
</head>
<body>
<div id="box">顶部</div>
<script>
//获取页面元素
var o_box = document.querySelector('#box');
//1. 求滚动条到顶端的距离
//2. 触发滚动事件 onscroll
//添加事件
window.onscroll = function(){
// console.log('呵呵');
// 求滚动条到顶端的距离
// 除了低版本谷歌都支持
// var scrollTop = document.documentElement.scrollTop;
//低版本谷歌
// var scrollTop = document.body.scrollTop;
//兼容
var scrollTop = Math.floor(document.documentElement.scrollTop || document.body.scrollTop);
console.log(scrollTop);
if(scrollTop >= 3000){
o_box.style.display = 'block';
}else{
o_box.style.display = 'none';
}
}
//给box添加一个点击事件
o_box.onclick = function(){
document.documentElement.scrollTop = document.body.scrollTop = 0;
}
</script>
</body>
</html>顶部悬浮<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body{
height: 5000px;
}
#box{
width: 100%;
height: 100px;
background: red;
position: absolute;
top: 400px;
}
</style>
</head>
<body>
<div id="box"></div>
<script>
//获取box元素
var o_box = document.querySelector('#box');
//添加事件
window.onscroll = function(){
//滚动条到顶端的距离
var scrollTop = Math.floor(document.documentElement.scrollTop || document.body.srcollTop);
//判断距离
if(scrollTop >= 400){
o_box.style.position = 'fixed';
o_box.style.left = 0;
o_box.style.top = 0;
}else{
o_box.style.position = 'absolute';
o_box.style.top = 400 + 'px';
}
}
</script>
</body>
</html>
Ned
Javascript中如何实现函数缓存?函数缓存有哪些应用场景?
一、是什么函数缓存,就是将函数运算过的结果进行缓存本质上就是用空间(缓存存储)换时间(计算过程)常用于缓存数据计算结果和缓存对象const add = (a,b) => a+b;
const calc = memoize(add); // 函数缓存
calc(10,20);// 30
calc(10,20);// 30 缓存缓存只是一个临时的数据存储,它保存数据,以便将来对该数据的请求能够更快地得到处理二、如何实现实现函数缓存主要依靠闭包、柯里化、高阶函数,这里再简单复习下:闭包闭包可以理解成,函数 + 函数体内可访问的变量总和(function() {
var a = 1;
function add() {
const b = 2
let sum = b + a
console.log(sum); // 3
}
add()
})()add函数本身,以及其内部可访问的变量,即 a = 1,这两个组合在⼀起就形成了闭包柯里化把接受多个参数的函数转换成接受一个单一参数的函数// 非函数柯里化
var add = function (x,y) {
return x+y;
}
add(3,4) //7
// 函数柯里化
var add2 = function (x) {
//**返回函数**
return function (y) {
return x+y;
}
}
add2(3)(4) //7将一个二元函数拆分成两个一元函数高阶函数通过接收其他函数作为参数或返回其他函数的函数function foo(){
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();//2函数 foo 如何返回另一个函数 bar,baz 现在持有对 foo 中定义的bar 函数的引用。由于闭包特性,a的值能够得到下面再看看如何实现函数缓存,实现原理也很简单,把参数和对应的结果数据存在一个对象中,调用时判断参数对应的数据是否存在,存在就返回对应的结果数据,否则就返回计算结果如下所示const memoize = function (func, content) {
let cache = Object.create(null)
content = content || this
return (...key) => {
if (!cache[key]) {
cache[key] = func.apply(content, key)
}
return cache[key]
}
}调用方式也很简单const calc = memoize(add);
const num1 = calc(100,200)
const num2 = calc(100,200) // 缓存得到的结果过程分析:在当前函数作用域定义了一个空对象,用于缓存运行结果运用柯里化返回一个函数,返回的函数由于闭包特性,可以访问到cache然后判断输入参数是不是在cache的中。如果已经存在,直接返回cache的内容,如果没有存在,使用函数func对输入参数求值,然后把结果存储在cache中三、应用场景虽然使用缓存效率是非常高的,但并不是所有场景都适用,因此千万不要极端的将所有函数都添加缓存以下几种情况下,适合使用缓存:对于昂贵的函数调用,执行复杂计算的函数对于具有有限且高度重复输入范围的函数对于具有重复输入值的递归函数对于纯函数,即每次使用特定输入调用时返回相同输出的函数new操作符具体干了什么?一、是什么在JavaScript中,new操作符用于创建一个给定构造函数的实例对象例子function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const person1 = new Person('Tom', 20)
console.log(person1) // Person {name: "Tom", age: 20}
t.sayName() // 'Tom'从上面可以看到:new 通过构造函数 Person 创建出来的实例可以访问到构造函数中的属性new 通过构造函数 Person 创建出来的实例可以访问到构造函数原型链中的属性(即实例与构造函数通过原型链连接了起来)现在在构建函数中显式加上返回值,并且这个返回值是一个原始类型function Test(name) {
this.name = name
return 1
}
const t = new Test('xxx')
console.log(t.name) // 'xxx'
可以发现,构造函数中返回一个原始值,然而这个返回值并没有作用
下面在构造函数中返回一个对象
function Test(name) {
this.name = name
console.log(this) // Test { name: 'xxx' }
return { age: 26 }
}
const t = new Test('xxx')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'从上面可以发现,构造函数如果返回值为一个对象,那么这个返回值会被正常使用二、流程从上面介绍中,我们可以看到new关键字主要做了以下的工作:创建一个新的对象obj将对象与构建函数通过原型链连接起来将构建函数中的this绑定到新建的对象obj上根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理举个例子:function Person(name, age){
this.name = name;
this.age = age;
}
const person1 = new Person('Tom', 20)
console.log(person1) // Person {name: "Tom", age: 20}
t.sayName() // 'Tom'三、手写new操作符现在我们已经清楚地掌握了new的执行过程那么我们就动手来实现一下newfunction mynew(Func, ...args) {
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype
// 3.将构建函数的this指向新对象
let result = Func.apply(obj, args)
// 4.根据返回值判断
return result instanceof Object ? result : obj
}测试一下function mynew(func, ...args) {
const obj = {}
obj.__proto__ = func.prototype
let result = func.apply(obj, args)
return result instanceof Object ? result : obj
}
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () {
console.log(this.name)
}
let p = mynew(Person, "huihui", 123)
console.log(p) // Person {name: "huihui", age: 123}
p.say() // huihui可以发现,代码虽然很短,但是能够模拟实现new
Ned
用three.js搞3个炫酷粒子出场
1.画出模型的点 this.loadModel('apple.glb').then((model) => {
let obj = model.children[0].children[0];
let geometry = obj.geometry;
//放大两倍
geometry.scale(2, 2, 2);
//形状点整体居中
geometry.center();
//渲染方式为画点
let material = new THREE.PointsMaterial({
size: 1,//点大小
color: new THREE.Color(that.color), //颜色
transparent: true, //开启透明
});
this.mesh = new THREE.Points(geometry, material);
this.scene.add(this.mesh);
//设置视角
this.setView(that.cameraPos, that.controlsPos);
})为了操作点方便,所以这里使用的是BufferGeometry,以上是苹果模型的BufferGeometry,如果是一些像球体等封装好的图元,可以采用以下方式转换成对应的缓存图元 //转为缓存图元
geometry = new THREE.BufferGeometry().fromGeometry(geometry);2.画圆点可以看到原始的点是正方形的,为了好看,我们需要将点变成圆形,那么我们需要在编译前修改片元着色器 material.onBeforeCompile = (shader) => {
console.log(shader)
})打印shader.fragmentShader可以看到编译前的代码,有点多和复杂,不用读懂,我们只需要替换其中要用的一句代码,变成我们需要的结果。 material.onBeforeCompile = (shader) => {
//修改片元着色器
shader.fragmentShader = shader.fragmentShader.replace(
`gl_FragColor = vec4( outgoingLight, diffuseColor.a );`,
` //需要替换的代码……`
);以下是画圆点的着色器,实现逻辑:计算每个片元离中心点的距离,远离中心点的片元没有过颜色//计算离中心点距离
float d=distance(gl_PointCoord, vec2(0.5, 0.5));
//离中心点0.5以外没有颜色
if(d>0.5) discard;
//复用替换前的代码
gl_FragColor = vec4(outgoingLight , diffuseColor.a );3.圆点变光点一个个圆点貌似有点丑,巴啦啦小魔仙仙,呜呼啦呼,变!float d=distance(gl_PointCoord, vec2(0.5, 0.5));
if(d < 0.3){//保持原色
gl_FragColor = diffuseColor;
}else{
//透明渐变
gl_FragColor.rgb = diffuseColor.rgb;
float cd =(1.0-d*2.0);
gl_FragColor.a=diffuseColor.a*cd*0.5;
}离中心点0.3以内的保持原来颜色其他部分根据离中心点距离透明渐变但是你会发现当点出现叠加时,有个黑框正方形,那是因为深度冲突的问题,这时候就需要关闭材质的深度测试 let material = new THREE.PointsMaterial({
size: 1,
color: new THREE.Color(that.color),
transparent: true,
depthTest: false//关闭深度测试
});可以看到黑框不见了,发光点正常了!4.出场方式1:让点从底部逐渐上升获取顶点位置,然后复制存一份作为原始值,另一份设置成全部点落在底geometry.boundingBox:形状的包围框,可以获取底部的位置和顶部的位置const positions = geometry.attributes.position;
const pos = positions.clone();
//底部位置
const bottom = geometry.boundingBox.min.y;
this.distance = bottom;
//顶部位置
this.max = geometry.boundingBox.max.y;
const count = pos.count;
for (let i = 0; i < count; i++) {
//将y轴坐标全部置底
pos.setXYZ(i, pos.getX(i), bottom, pos.getZ(i));
}
pos.needsUpdate = true;
geometry.setAttribute('position', pos);
geometry.setAttribute('initialPosition', positions.clone());
geometry.attributes.position.setUsage(THREE.DynamicDrawUsage);可以看到3D形状像是变成扁平二维了动起来,让世界变得精彩this.speed:运动速度this.speed1:运动加速度this.distance:上升的距离动画逻辑:当初始点小于上升距离时,则点回到原来的位置当点大于上升距离时,点维持上升距离的位置上升距离随着时间增大,这样就可以呈现沿着一个平面,点逐步复原的出场效果!当上升距离大于最高点则结束动画,出场完成!animateAction() {
if (this.mesh && this.distance <= this.max) {
this.speed += this.speed1;
this.distance += this.speed;
let dist = this.distance;
const positions = this.mesh.geometry.attributes.position;
const initialPositions = this.mesh.geometry.attributes.initialPosition;
const count = positions.count;
let t = this.max - this.distance;
for (let i = 0; i < count; i++) {
const iy = initialPositions.getY(i);
positions.setXYZ(i, positions.getX(i), iy <= dist ? iy : dist, positions.getZ(i));
}
//通知材质的着色器,点要更新
positions.needsUpdate = true;
}
}注意:赋值改变点位置后,一定要positions.needsUpdate = true;通知点位置属性要更新5.出场方式2:让凌乱的点汇聚逻辑跟第一种出场方式类似,不过这里需要存储一份随机偏移值,用来生成凌乱的点const positions = geometry.attributes.position;
geometry.setAttribute('initialPosition', positions.clone());
const pos = positions.clone();
const count = pos.count;
const displacement = new Float32Array(count);
for (let i = 0; i < count; i++) {
//随机偏移值
displacement[i] = that.minDistance + that.distance * Math.random();
}
pos.needsUpdate = true;
geometry.setAttribute('position', pos);
//偏移值赋值
geometry.setAttribute('displacement', new THREE.BufferAttribute(displacement, 1));
geometry.attributes.position.setUsage(THREE.DynamicDrawUsage);
动画逻辑:初始点根据时间变换,逐渐减少偏移,最终回调原始的点,形成3D形状当偏移距离减少至零,全部点恢复位置,动画结束,出场完成!this.time时间增长值注意:偏移距离要乘以法向量,这样才能让点四面八方地分布 animateAction() {
if (this.mesh && this.time >= 0) {
this.speed += this.speed1;
this.time += this.speed;
const positions = this.mesh.geometry.attributes.position;
const normal = this.mesh.geometry.attributes.normal;
const initialPositions = this.mesh.geometry.attributes.initialPosition;
const displacement = this.mesh.geometry.attributes.displacement;
const count = positions.count;
let t = 2.0 - this.time;
for (let i = 0; i < count; i++) {
//计算该时间的偏移距离
const d = displacement.getX(i) * t;
const ix = initialPositions.getX(i);
const iy = initialPositions.getY(i);
const iz = initialPositions.getZ(i);
const nx = normal.getX(i);
const ny = normal.getY(i);
const nz = normal.getZ(i);
//初始点减去偏移距离
positions.setXYZ(i, ix - nx * d, iy - ny * d, iz - nz * d);
}
positions.needsUpdate = true;
if (this.time >= 2) {//结束动画
this.time = -1;
}
}
}6.出场方式3:中心爆炸点这个与上面的出场方式类似,不过是全部点从一个点出发,然后回到原来的位置,于是就要设置初始点为同一个位置const positions = geometry.attributes.position;
const b = geometry.boundingBox;
this.max = Math.max(
Math.abs(b.min.x),
Math.abs(b.min.y),
Math.abs(b.min.z),
Math.abs(b.max.x),
Math.abs(b.max.y),
Math.abs(b.max.z)
);
geometry.setAttribute('initialPosition', positions.clone());
const pos = positions.clone();
const count = pos.count;
for (let i = 0; i < count; i++) {
//全部点设置为原点
pos.setXYZ(i, 0, 0, 0);
}
pos.needsUpdate = true;
是是
geometry.setAttribute('position', pos);
geometry.attributes.position.setUsage(THREE.DynamicDrawUsage);动画逻辑:半径随着时间增大,当点原始位置与原点的距离小于半径则回归原位,否则跟着扩张半径运动,直至全部点回到原始位置,形成最终的3D形状!半径扩展与时间是比例相乘关系,当时间为1时,则恢复原状,动画结束,出场完成!注意:每个点与原点的距离有正负值之分,对比时要用绝对值! animateAction() {
if (this.mesh && this.time >= 0) {
this.speed += this.speed1;
this.time += this.speed;
const positions = this.mesh.geometry.attributes.position;
const normal = this.mesh.geometry.attributes.normal;
const initialPositions = this.mesh.geometry.attributes.initialPosition;
const count = positions.count;
const radius = this.time * this.max;
for (let i = 0; i < count; i++) {
const nx = normal.getX(i);
const ny = normal.getY(i);
const nz = normal.getZ(i);
const ix = initialPositions.getX(i);
const iy = initialPositions.getY(i);
const iz = initialPositions.getZ(i);
positions.setXYZ(
i,
radius >= Math.abs(ix) ? ix : radius * nx,
radius >= Math.abs(iy) ? iy : radius * ny,
radius >= Math.abs(iz) ? iz : radius * nz
);
}
positions.needsUpdate = true;
if (this.time >= 1) {
this.time = -1;
}
}
}7.给光点开启布灵布灵的效果传入每个点的颜色值,这里采用的是随机颜色,范围是[0,1]
let colors = [];
for (let i = 0; i < positions.count; i++) {
colors.push(Math.random(), Math.random(), Math.random());
}
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
开启材质的顶点颜色let material = new THREE.PointsMaterial({
size: 1,
color: new THREE.Color(that.color),
vertexColors: true,//顶点颜色
transparent: true,
depthTest: false
});完成出场后,动画帧中添加给每个点不停赋值随机色,就能形成一闪一闪的效果。 const colors = this.mesh.geometry.attributes.color;
const count = colors.count;
for (let i = 0; i < count; i++) {
colors.setXYZ(i, Math.random(), Math.random(), Math.random());
}
colors.needsUpdate = true;注意:点颜色值改变要通知颜色属性要更新为什么开启点颜色后可以有不同颜色深度的效果呢?我们可以改一下编译前的片元着色器,去掉一个分号,让它报错,打印一下编译后的着色器结果material.onBeforeCompile = (shader) => {
//修改片元着色器,使其变成发光圆点
shader.fragmentShader = shader.fragmentShader.replace(
`gl_FragColor = vec4( outgoingLight, diffuseColor.a );`,
`gl_FragColor = vec4( outgoingLight, diffuseColor.a )`
);
};2. 代码真的好多,看得好头大!是时候展现你的着色器常识了!这个点颜色值肯定是从顶点着色器那边穿过来的,搜一下varying全局变量,果不其然,可以发现一下代码!173: #ifdef USE_COLOR
174: varying vec3 vColor;
175: #endif
229: #ifdef USE_COLOR
230: diffuseColor.rgb *= vColor;
231: #endif3. 破案!diffuseColor是点显示的颜色,默认的时候是材质的color属性值,如果开启vertexColors后,vColor传过来,会执行颜色值相乘,即颜色值叠加,就会出现这样不同的深度的颜色。总结以上点的运动都是通过计算传入最终位置结果,但其实可以通过修改顶点着色器也能实现同样的效果。three.js真的封装很全,大家可以弄点报错,看看人家的着色器代码,学习一下,也方便以后修改着色器代码,自定义效果!GitHub地址https://github.com/xiaolidan00/my-three参考:threejs.org/examples/?q…
Ned
《Three.js开发指南第2版》读书笔记2
内容来源于书籍【美】乔斯.德克森著,杨芬、赵汝达 译的《Three.js开发指南第2版》 以下为读书笔记,仅供学习随书源码地址:https://github.com/josdirksen/learning-threejs[TOC]第 3 章 学习使用 Three.js 中的光源不同种类的光源名字描述THREE. AmbientLight基本光源,该光源的颜色将会叠加到场景现有物体的颜色上THREE. PointLight点光源,从空间的一个点向所有方向发射光线,点光源不能用来创建阴影THREE. SpotLight光源有聚光的效果,类似台灯、天花板上的吊灯或手电筒,这种光源可以投射阴影THREE. DirectionalLIght无线光,光源发出的光线可以看作是平行,类似太阳光,可创建阴影THREE. HemisphereLight特殊光源,可通过模拟反光面和光线微弱的天空来创建更加自然的室外光线,不提供阴影相关功能THREE. AreaLight可指定散发光线的平面,不是一个点,不投射阴影THREE. LensFlare不是光源,可为场景中的光源家镜头光晕效果THREE. AmbientLight颜色将应用到全局,将场景中所有物体渲染为相同颜色THREE. SpotLight 光源来产生阴影THREE. Color函数名描述set(value)十六进制颜色值,可以是字符串,数值,THREE. ColorsetHex(value)十六进制数字值setRGB(r, g, b)RGB 值颜色,范围 0~1setHSL(h, s, l)HSL 值颜色,范围 0~1setStyle(style)css 颜色copy(color)复制颜色对象copyGammaToLinear(color)伽马色彩转换到线性色彩,内部使用copyLinearToGamma(color)线性色彩转换到伽马色彩,内部使用convertGammaToLinear(color)伽马色彩转换到线性色彩convertLinearToGammacolor)线性色彩转换到伽马色彩getHex()十六进制值getHexString()十六进制字符串getStyle()css 颜色getHSL(optionalTarget)HSL 颜色值offsetHSL(h, s, l)将 h, s, l 添加到当前颜色 h, s, ladd(color)将 r, g, b 添加到当前颜色addColors(color1, color2)内部使用,color1, color2 相加addScalar(s)内部使用,当前颜色 RGB 分量上multiply(color)内部使用,当前颜色与 THREE.color 对象的 RGB 值相乘multiplyScalar(s)内部使用,当前颜色与 RGB 值相乘lerp(color, alpha)内部使用,找出介于对象和颜色和提供颜色之间的颜色,alpha 定义当前颜色与提供颜色的差距lerp(color, alpha)内部使用,找出介于对象和颜色和提供颜色之间的颜色,alpha 定义当前颜色与提供颜色的差距equals(color)THREE. Color 颜色对象的 RGB 值与当前值相等与否fromArray(array)与 setRGB 方法具有相同的功能,只是 RGB 值通过数字数组传入toArray返回三元素组[r, g, b]clone()复制当前颜色THREE. PointLightTHREE. PointLight 从特定的一点向所有方向发射光线,点光源属性描述color(颜色)光源颜色distance(距离)光源照射的距离,默认 0,以为光的强度不会随距离增加而减少intensity(强度)光源照射的强度, 默认 1position(位置)光源在场景中的位置visible(是否可见)true 光源打开,false 光源关闭var pointColor = '#ccffcc';
var pointLight = new THREE.PointLight(pointColor);
pointLight.position.set(10, 10, 10);
pointLight.intensity = 2.4;
pointLight.distance = 100;
scene.add(pointLight);THREE. SpotLight从特定一点发射锥形光线,聚光灯光源 其他属性同 THREE. PointLight属性描述angle(角度)光源发射出的光速宽度,单位弧度, 默认值 Math. PI/3caseShadow(投影)光源是否产生投影exponent(光强衰减指数)光线强度递减的速度onlyShadow(仅阴影)光源只生成阴影,不会再场景中添加任何光照shadowBias(阴影偏移)偏置阴影的位置,默认 0shadowCameraFar(投影远点)到距离光源光源那个位置可生成阴影,默认值为 5000shadowCameraFov(投影视场)用于生成阴影的视场有多大,默认值为 50shadowCameraNear(投影近点)到距离光源光源那个位置开始生成阴影,默认值为 50shadowCameraVisible(阴影方式可见)光源阴影设置可见与否shadowDarkness(阴影暗度)阴影渲染的暗度,在场景渲染后无法更改,默认 0.5shadowMapWidth, shadowMapHeight(阴影映射宽度和高度)决定有多少像素用来生成阴影,若阴影锯齿状或不光滑,增加此值,渲染后无法更改,默认 512target(目标)光源只想场景中的特定对象或位置var pointColor = '#ffffff';
var spotLight = new THREE.spotLight(pointColor);
spotLight.position.set(-40, 60, -10);
spotLight.castShadow = true;
spotLight.target = plane;
var target = new THREE.Object3D();
target.position = new THREE.Vector3(5, 0, 0);
spotLight.target = target;
scene.add(spotLight);阴影使用可能遇到的问题阴影模糊,增加 shadowMapWidth 和 shadowMapHeight,或保证用于计算阴影区域紧密包围在对象周围(shadowCameraNear, shadowCameraFar, shadowCameraFov)产生阴影与接收阴影设置,光源生成阴影,几何体是否接收或投射阴影 castShadow 和 receiveShadow薄对象渲染阴影时可能出现奇怪的渲染失真,可通过 shadowBias 轻微偏移阴影来修复调整 shadowDarkness 来改变阴影的暗度阴影更柔和,可在 THREE. WebGLRenderer 设置不同 shadowMapType。默认 THREE. PCFShadowMap, 柔和:PCFSoftShadowMapTHREE. DirectionalLisht从而为平面发射光线,光线彼此平行,平行光源,光强是一样的 其他属性同 SpotLight//阴影区域
directionalLight.shadowCameraNear = 2;
directionalLight.shadowCameraFar = 200;
directionalLight.shadowCameraLeft = -50;
directionalLight.shadowCameraRight = 50;
directionalLight.shadowCameraTop = 50;
directionalLight.shadowCameraBottom = -50;
//创建更好的阴影效果
directionalLight.shadowCascade = true;
//将阴影生成分裂,靠近摄像机十点会产生更具细节的阴影,远离摄像机十点阴影的细节更少
directionalLight.shadowCascadeCount;
shadowCascadeBias;
shadowCascadeWidth;
shadowCascadeHeight;
shadowCascadeNearZ;
shadowCascadeFarZ;THREE. HemisphereLight户外光照效果var hemiLight = new THREE.HemisphereLight(0x0000ff, 0x00ff00, 0.6);
hemiLight.position.set(0, 500, 0);
scene.add(hemiLight);属性描述groundColor从地面发出的光线颜色color从天空发出的光线颜色intensity光线照射强度THREE. AreaLight长方体的发光区域 使用 WebGLDeferredRenderer<script type="text/javascript" src="../libs/WebGLDeferredRenderer.js"></script>
<script type="text/javascript" src="../libs/ShaderDeferred.js"></script>
<script type="text/javascript" src="../libs/RenderPass.js"></script>
<script type="text/javascript" src="../libs/EffectComposer.js"></script>
<script type="text/javascript" src="../libs/CopyShader.js"></script>
<script type="text/javascript" src="../libs/ShaderPass.js"></script>
<script type="text/javascript" src="../libs/FXAAShader.js"></script>
<script type="text/javascript" src="../libs/MaskPass.js"></script>var renderer = new THREE.WebGLDeferredRenderer({
width: window.innerWidth,
height: window.innerHeight,
scale: 1,
antialias: true,
tonemapping: THREE.FilmicOperator,
brightness: 2.5,
});var areaLight1 = new THREE.AreaLight(0xff0000, 3);
areaLight1.position.set(-10, 10, -35);
areaLight1.rotation.set(-Math.PI / 2, 0, 0);
areaLight1.width = 4;
areaLight1.height = 9.9;
scene.add(areaLight1);
var planeGeometry1 = new THREE.BoxGeometry(4, 10, 0);
var planeGeometry1Mat = new THREE.MeshBasicMaterial({
color: 0xff0000
});
var plane1 = new THREE.Mesh(planeGeometry1, planeGeometry1Mat);
//在areaLight相同位置 防止对象来模拟光线照射的区域
plane1.position.copy(areaLight1.position);
scene.add(plane1);THREE. LensFlare镜头光晕flare = new THREE.LensFlare(texture, size, distance, blending, color, opacity);参数描述texture(纹理)图片纹理,用来决定光晕的形状size(尺寸)光晕大小,单位像素,值-1,使用纹理本身的尺寸distance(距离)从光源 0 到摄像机 1 的距离,将镜头光晕防止在正确位置blending(混合)为光晕提供多种材质,混合材质,默认 THREE. AdditiveBlendingcolor(颜色)光晕颜色var textureFlare0 = THREE.ImageUtils.loadTexture('../assets/textures/lensflare/lensflare0.png');
var textureFlare3 = THREE.ImageUtils.loadTexture('../assets/textures/lensflare/lensflare3.png');
var flareColor = new THREE.Color(0xffaacc);
var lensFlare = new THREE.LensFlare(textureFlare0, 350, 0.0, THREE.AdditiveBlending, flareColor);
lensFlare.add(textureFlare3, 60, 0.6, THREE.AdditiveBlending);
lensFlare.add(textureFlare3, 70, 0.7, THREE.AdditiveBlending);
lensFlare.add(textureFlare3, 120, 0.9, THREE.AdditiveBlending);
lensFlare.add(textureFlare3, 70, 1.0, THREE.AdditiveBlending);
lensFlare.position.copy(spotLight.position);
scene.add(lensFlare);第 4 章 使用 Three.js 的材质名称描述MeshBasicMaterial(网格基础材质)给几何体赋予一种简单颜色,可显示几何体线框MeshDepthMaterial(网格深度材质)从摄像机到网格的距离决定如何给网格上色MeshNormalMaterial(网格法向材质)根据法向量计算物体表面的颜色MeshFaceMaterial(网格面材质)容器,可为几何体的各个表面指定不同的材质MeshLambertMaterial(网格 Lambert 材质)考虑光照影响,用于创建暗淡、不光亮的物体MeshPhongMaterial(网格 Phong 式材质)考虑光照影响,用于创建光亮的物体ShaderMaterial(着色器材质)允许自定义的着色器程序,直接控制顶点的繁殖方式和像素的着色方式,与 THREE. BufferedGeometry 一起使用LineBaseMaterial(直线基础材质)用于 THREE. Line 几何体,创建着色的直线LineDashMaterial(虚线材质)同上,允许创建出虚线效果分类基础属性:最常用,可控制物体的不透明度,是否可见以及被应用融合属性:每个物体都有一系列的融合属性,决定物体与背景的融合高级属性:可控制底层 WebGL 上下午对象渲染物体的方式基础属性 THREE. Material属性描述id(标识符)用来识别材质,并在材质创建时复制,第一个材质的值从 0 开始,每新增一个材质这个值加 1uuid(通用唯一识别码)生成的唯一 ID, 内部使用name(名称)给材质赋予名称,用于调试的目的opacity(不透明度)定义物体的透明度,与 transparent 一起使用,范围 0~1transparent(是否透明)true: 使用指定的不透明度的渲染物体,false: 物体不透明只是着色更加明亮,若使用 alpha 通道的纹理,设置为 trueoverdraw(过度描绘)THREE. CanvasRender 时多边形会被渲染得稍微大一点,设 truevisible(是否可见)材质是否可见,false 场景中不见该物体side(侧面)几何体的哪一面应用材质,默认 THREE. FrontSide(前面,外侧), THREE. BackSide(背面,内侧), THREE. DoubleSide(双侧)needsUpdate(是否更新)true: 更新材质属性混合属性名称描述blending(融合)物体上材质与背景如何融合,一般融合模式 THREE. NormalBlending,只显示材质上层blendsrc(融合源)自定义融合模式,物体(源),背景(目标),默认 THREE. SrcAlphaFactor,使用 alpha 透明度通道融合blenddst(融合目标)默认 THREE. OneMinusSrcAlphaFactor, 也是用透明度融合,值 1blendequation(融合公式)默认相加 AddEquation高级属性名称描述depthTest可打开 GL_DEPTH_TEST 参数,控制是否使用像素深度来计算心像素值depthWrite内部属性,用来决定材质是否影响 WebGL 深度缓存,二维贴图时设 falsepolygonOffset, polygonOffsetFactor, polygonOffsetUnits控制 WebGL 的 POLYGON_OFFSET_FILL 特性alphatest范围 0~1,某个像素小于该值则不显示,移除一些与透明度相关的毛边MeshBaseMaterialvar meshMaterial = new THREE.MeshBasicMaterial({
color: 0x7777ff
});
var meshMaterial = new MeshBasicMaterial();
material.color = new THREE.Color(0xff0000);名称描述color(颜色)设置材质的颜色wireframe(线框)将材质渲染成线框wireframeLinewidth(线框线宽)线框中线的宽度wireframeLinecap(线框线段端点)可选 butt 平, round 圆, square 方,默认 round,WebGLRenderer 不支持wireframeLinejoin(线框线段连接点)同上shading(着色)可选 THREE. SmoothShading(默认), THREE. NoShading, THREE. FlatShadingvertexColors(顶点颜色)默认 THREE. NoColors, 设置后采用 THREE. Geometry 对象的 colors 属性 CanvasRenderer 不起作用fog(雾化)是否受全局雾化效果影响THREE. MeshDepthMaterial名称描述wireframe(线框)是否显示线框wireframeLinewidth(线框线宽)线框中线的宽度联合材质var cubeMaterial = new THREE.MeshDepthMaterial();
var colorMaterial = new THREE.MeshBasicMaterial({
color: controls.color,
transparent: true, //
blending: THREE.MultiplyBlending, //混合模式,与背景相互作用
});
var cube = new THREE.SceneUtils.createMultiMaterialObject(cubeGeometry, [
colorMaterial,
cubeMaterial,
]);
cube.children[1].scale.set(0.99, 0.99, 0.99); //避免画面闪烁THREE. MeshNormalMaterial使用 THREE. ArrowHelper 添加法向量for (var f = 0, fl = sphere.geometry.faces.length; f < fl; f++) {
var face = sphere.geometry.faces[f];
var centroid = new THREE.Vector3(0, 0, 0);
centroid.add(sphere.geometry.vertices[face.a]);
centroid.add(sphere.geometry.vertices[face.b]);
centroid.add(sphere.geometry.vertices[face.c]);
centroid.divideScalar(3);
var arrow = new THREE.ArrowHelper(face.normal, centroid, 2, 0x3333ff, 0.5, 0.5);
sphere.add(arrow);
}名称描述wireframe(线框)是否显示线框wireframeLinewidth(线框线宽)线框中线的宽度shading设置着色方法,THREE. FlatShading 平面着色, THREE. SmoothShadding 平滑着色THREE. MeshFaceMaterial材质容器,允许集合体的每个面指定不同的材质var mats = [];
mats.push(new THREE.MeshBasicMaterial({
color: 0x009e60
}));
mats.push(new THREE.MeshBasicMaterial({
color: 0x009e60
}));
mats.push(new THREE.MeshBasicMaterial({
color: 0x0051ba
}));
mats.push(new THREE.MeshBasicMaterial({
color: 0x0051ba
}));
mats.push(new THREE.MeshBasicMaterial({
color: 0xffd500
}));
mats.push(new THREE.MeshBasicMaterial({
color: 0xffd500
}));
mats.push(new THREE.MeshBasicMaterial({
color: 0xff5800
}));
mats.push(new THREE.MeshBasicMaterial({
color: 0xff5800
}));
mats.push(new THREE.MeshBasicMaterial({
color: 0xc41e3a
}));
mats.push(new THREE.MeshBasicMaterial({
color: 0xc41e3a
}));
mats.push(new THREE.MeshBasicMaterial({
color: 0xffffff
}));
mats.push(new THREE.MeshBasicMaterial({
color: 0xffffff
}));
var faceMaterial = new THREE.MeshFaceMaterial(mats);
//魔方
for (var x = 0; x < 3; x++) {
for (var y = 0; y < 3; y++) {
for (var z = 0; z < 3; z++) {
var cubeGeom = new THREE.BoxGeometry(2.9, 2.9, 2.9);
var cube = new THREE.Mesh(cubeGeom, faceMaterial);
cube.position.set(x * 3 - 3, y * 3, z * 3 - 3);
group.add(cube);
}
}
}THREE. MeshPhongMaterial创建光亮材质,属性与暗淡材质 THREE. MeshLambertMaterial 基本属性一样名称描述ambient环境色,颜色与环境色相乘,默认白色emissive材质发射的颜色,默认黑色,不收其他光照影响的颜色specular指定材质光亮程度及高光部分的颜色,设为与color相同颜色,类似金属材质,设为灰色grey则更像塑料shininess镜面高光部分的亮度,默认值30metaltrue金属效果,效果微弱wrapAroundtrue启用办lambert光照技术,光下降更微妙,网格粗糙黑暗地区莹阴影柔和且分布更加均匀wrapRGBtrue可使用THREE. Vector3控制光下降的速度THREE. ShaderMaterial 创建自己的着色器着色器可讲Three.js中的JavaScript网格转换为屏幕上的像素,可指定对象如何渲染,如何覆盖或修改Three.js库中的默认值名称描述wireframe是否显示线框wireframeLinewidth线框中线的宽度shading设置着色方法,THREE. FlatShading 平面着色, THREE. SmoothShadding 平滑着色vertexColors给每个顶点定义不同的颜色,对CanvasRenderer不起作用,对WebGLRenderer起作用fog是否受全局无话效果影响名称描述fragmentShader定义的每个传入像素的颜色(必传)vertexShader允许你修改传入的定点位置(必传)uniforms可想你的着色器发信息,同样的信息会发送给每一个顶点和片段defines转换成#define代码片段,这些片段可以设置着着色程序里的额外全局变量attributes可修改每个顶点和片段,用来传递每个位置数据和法向量相关的数据lights定义光照数据是否传递给着色器,默认falsevertexShader:它会在几何体的每个顶点上执行,可用这个着色器改变顶点的位置来对集合体进行变换fragmentShader: 它会在集合体的每一个片段上执行,在VertexShader里,会返回这个特定片段应该显示的颜色<script id="vertex-shader" type="x-shader/x-vertex">
uniform float time;
varying vec2 vUv;
void main()
{
vec3 posChanged = position;
posChanged.x = posChanged.x*(abs(sin(time*1.0)));
posChanged.y = posChanged.y*(abs(cos(time*1.0)));
posChanged.z = posChanged.z*(abs(sin(time*1.0)));
//gl_Position = projectionMatrix * modelViewMatrix * vec4(position*(abs(sin(time)/2.0)+0.5),1.0);
gl_Position = projectionMatrix * modelViewMatrix * vec4(posChanged,1.0);
}
</script>function createMaterial(vertexShader, fragmentShader) {
var vertShader = document.getElementById(vertexShader).innerHTML;
var fragShader = document.getElementById(fragmentShader).innerHTML;
var attributes = {};
var uniforms = {
time: {
type: 'f',
value: 0.2
},
scale: {
type: 'f',
value: 0.2
},
alpha: {
type: 'f',
value: 0.6
},
resolution: {
type: "v2",
value: new THREE.Vector2()
}
};
uniforms.resolution.value.x = window.innerWidth;
uniforms.resolution.value.y = window.innerHeight;
var meshMaterial = new THREE.ShaderMaterial({
uniforms: uniforms,
attributes: attributes,
vertexShader: vertShader,
fragmentShader: fragShader,
transparent: true
});
return meshMaterial;
}
var cubeGeometry = new THREE.BoxGeometry(20, 20, 20);
var meshMaterial1 = createMaterial("vertex-shader", "fragment-shader-1");
var meshMaterial2 = createMaterial("vertex-shader", "fragment-shader-2");
var meshMaterial3 = createMaterial("vertex-shader", "fragment-shader-3");
var meshMaterial4 = createMaterial("vertex-shader", "fragment-shader-4");
var meshMaterial5 = createMaterial("vertex-shader", "fragment-shader-5");
var meshMaterial6 = createMaterial("vertex-shader", "fragment-shader-6");
var material = new THREE.MeshFaceMaterial(
[meshMaterial1,
meshMaterial2,
meshMaterial3,
meshMaterial4,
meshMaterial5,
meshMaterial6
]);
var cube = new THREE.Mesh(cubeGeometry, material);THREE. LineBaseMaterial用于线段的基础材质名称描述color线的颜色,指定vertexColors该属性会被忽略linewidth线的宽度linecap线段端点,可选只butt(平),round(圆),square(方),默认roundlinejoin线段连接,可选只butt(平),round(圆),square(方),默认roundvertexColor每个顶点指定一种颜色fog当前材质是否受全局无话效果影响var points = gosper(4, 60); //gosper曲线
var lines = new THREE.Geometry();
var colors = [];
var i = 0;
points.forEach(function(e) {
lines.vertices.push(new THREE.Vector3(e.x, e.z, e.y));
colors[i] = new THREE.Color(0xffffff);
colors[i].setHSL(e.x / 100 + 0.5, (e.y * 20) / 300, 0.8);
i++;
});
lines.colors = colors;
var material = new THREE.LineBasicMaterial({
opacity: 1.0,
linewidth: 1,
vertexColors: THREE.VertexColors
});
var line = new THREE.Line(lines, material);THREE. LineDashMaterial属性与THREE. LineBaseMaterial一样,可指定虚线与空白间隙的长度来创建出虚线效果名称描述scale放缩dashSize和gapSizedashSize虚线长度gapSize虚线间隔宽度 lines.computeLineDistances();
var material = new THREE.LineDashedMaterial({
vertexColors: true,
color: 0xffffff,
dashSize: 2,
gapSize: 2,
scale: 0.1
});
Ned
AMap+Three.js踩坑日记
最近有个项目要跟高德地图结合,2D功能和3D功能都要有,于是就用到AMap.GLCustomLayer,能让我用Three,那就可以放飞自我了,然而我头秃了!官方示例:developer.amap.com/demo/jsapi-…注意事项1.three.js版本问题我之前习惯124的,官方给出的是142,现在这两个版本没问题,不清楚其他版本行不行。2.中心点设置这会作为参考的原点,所有坐标相对于这个坐标的 map.customCoords.setCenter(mapConfig.center);3.不要在render里面搞事情直接按照官方照搬,render就是个帧渲染,在里面搞东西会导致你不停的重复操作,卡死你,把你的requestAnimationFrame动画提到外面
var gllayer = new AMap.GLCustomLayer({
// 图层的层级
zIndex: 10,
// 初始化的操作,创建图层过程中执行一次。
init: async (gl) => {
},
render: () => {
//直接按照官方照搬
}
});
map.add(gllayer);
// 动画
function animate() {
if (window.threeApp?.isStats) {
window.threeApp.stats.update();
}
if (window.TWEEN) {
window.TWEEN.update();
}
if (window.controls) {
window.controls.update();
}
map.render();
requestAnimationFrame(animate);
}
animate();4. 高德地图的视角控制你能采用center,zoom,pitch,rotation进行调控视角了,通过getView来获取当前视角,然后对应赋值。地图的视角切换已经自带动画了,TWEEN可以省了,优秀! cameraTo(c) {
this.map.setCenter(c.pos);
this.map.setZoom(c.zoom);
this.map.setPitch(c.pitch, true, 1000);
this.map.setRotation(c.rotate, true, 1000);
this.camera.position.x = c.camera.x;
this.camera.position.y = c.camera.y;
this.camera.position.z = c.camera.z;
}
getView() {
console.log('center', this.map.getCenter());
console.log('zoom', this.map.getZoom());
console.log('pitch', this.map.getPitch());
console.log('rotate', this.map.getRotation());
console.log('camera', this.camera.position);
}5.添加模型后发现模型不见了emmm~,打印scene,明明在的,咋就没踪没影了。1.除了要用customCoords.lngLatToCoord转换坐标位置,还要注意高德地图的xyz跟常用THREE的xyz有点不同THREE里面,z对应前后,x对应左右,y对应上下而AMAP里面,xy是对应经纬度,z对应的上下的位置,模型有可能需要转个角度Math.PI*0.5.2.老生常谈的注意模型大小,模型大小跟地图放缩是同步的,有可能因为这样而看不清。6.跟高德地图模型混合,但又不想清空附近的建筑模型这个问题我也不知道,求大神来解答。目前我要不只能用人家的建筑模型,要不就把建筑给关了showBuildingBlock: false。7.包围盒计算区别function getObjBox(object) {
if (object.updateMatrixWorld) {
object.updateMatrixWorld();
}
// 获得包围盒得min和max
const box = new THREE.Box3().setFromObject(object);
let objSize = box.getSize(new THREE.Vector3());
// 返回包围盒的中心点
const center = box.getCenter(new THREE.Vector3());
return {
size: objSize,
center,
box
};
}因为THREE与AMAP的y和z是不同的,然后就会THREE依旧遵循原来的规则,box的min,max,center,size的y和z记得要对调一下才是正确的后续开发中遇到问题,会继续更新~
Ned
掌握JavaScript继承的精髓:原型继承、构造函数继承以及组合继承的实现技巧
一、是什么继承(inheritance)是面向对象软件技术当中的一个概念。如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”继承的优点继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码在子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能虽然JavaScript并不是真正的面向对象语言,但它天生的灵活性,使应用场景更加丰富关于继承,我们举个形象的例子:定义一个类(Class)叫汽车,汽车的属性包括颜色、轮胎、品牌、速度、排气量等class Car{
constructor(color,speed){
this.color = color
this.speed = speed
// ...
}
}由汽车这个类可以派生出“轿车”和“货车”两个类,在汽车的基础属性上,为轿车添加一个后备厢、给货车添加一个大货箱// 货车
class Truck extends Car{
constructor(color,speed){
super(color,speed)
this.Container = true // 货箱
}
}这样轿车和货车就是不一样的,但是二者都属于汽车这个类,汽车、轿车继承了汽车的属性,而不需要再次在“轿车”中定义汽车已经有的属性在“轿车”继承“汽车”的同时,也可以重新定义汽车的某些属性,并重写或覆盖某些属性和方法,使其获得与“汽车”这个父类不同的属性和方法class Truck extends Car{
constructor(color,speed){
super(color,speed)
this.color = "black" //覆盖
this.Container = true // 货箱
}
}从这个例子中就能详细说明汽车、轿车以及卡车之间的继承关系二、实现方式下面给出JavaScripy常见的继承方式:原型链继承构造函数继承(借助 call)组合继承原型式继承寄生式继承寄生组合式继承原型链继承原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针举个例子 function Parent() {
this.name = 'parent1';
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child2';
}
Child1.prototype = new Parent();
console.log(new Child())上面代码看似没问题,实际存在潜在问题var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4]改变s1的play属性,会发现s2也跟着发生变化了,这是因为两个实例使用的是同一个原型对象,内存空间是共享的构造函数继承借助 call调用Parent函数function Parent(){
this.name = 'parent1';
}
Parent.prototype.getName = function () {
return this.name;
}
function Child(){
Parent1.call(this);
this.type = 'child'
}
let child = new Child();
console.log(child); // 没问题
console.log(child.getName()); // 会报错可以看到,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法组合继承前面我们讲到两种继承方式,各有优缺点。组合继承则将前两种方式继承起来function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
Parent3.prototype.getName = function () {
return this.name;
}
function Child3() {
// 第二次调用 Parent3()
Parent3.call(this);
this.type = 'child3';
}
// 第一次调用 Parent3()
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己的构造函数
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play); // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'这种方式看起来就没什么问题,方式一和方式二的问题都解决了,但是从上面代码我们也可以看到Parent3 执行了两次,造成了多构造一次的性能开销原型式继承这里主要借助Object.create方法实现普通对象的继承同样举个例子let parent4 = {
name: "parent4",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
let person4 = Object.create(parent4);
person4.name = "tom";
person4.friends.push("jerry");
let person5 = Object.create(parent4);
person5.friends.push("lucy");
console.log(person4.name); // tom
console.log(person4.name === person4.getName()); // true
console.log(person5.name); // parent4
console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]这种继承方式的缺点也很明显,因为Object.create方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能寄生式继承寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法let parent5 = {
name: "parent5",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
function clone(original) {
let clone = Object.create(original);
clone.getFriends = function() {
return this.friends;
};
return clone;
}
let person5 = clone(parent5);
console.log(person5.getName()); // parent5
console.log(person5.getFriends()); // ["p1", "p2", "p3"]其优缺点也很明显,跟上面讲的原型式继承一样寄生组合式继承寄生组合式继承,借助解决普通对象的继承问题的Object.create 方法,在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式function clone (parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
function Parent6() {
this.name = 'parent6';
this.play = [1, 2, 3];
}
Parent6.prototype.getName = function () {
return this.name;
}
function Child6() {
Parent6.call(this);
this.friends = 'child5';
}
clone(Parent6, Child6);
Child6.prototype.getFriends = function () {
return this.friends;
}
let person6 = new Child6();
console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6}
console.log(person6.getName()); // parent6
console.log(person6.getFriends()); // child5可以看到 person6 打印出来的结果,属性都得到了继承,方法也没问题文章一开头,我们是使用ES6 中的extends关键字直接实现 JavaScript的继承class Person {
constructor(name) {
this.name = name
}
// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以简写为 getName() {...}
getName = function () {
console.log('Person:', this.name)
}
}
class Gamer extends Person {
constructor(name, age) {
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
super(name)
this.age = age
}
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法利用babel工具进行转换,我们会发现extends实际采用的也是寄生组合继承方式,因此也证明了这种方式是较优的解决继承的方式
Ned
JavaScript单页面路由详解:打造现代化、高性能的Web应用
# 基于SPA的单页面路由关于单页应用单页Web应用(single page web application,SPA),就是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。简单来说就是用户只需要加载一次页面就可以不再请求,当点击其他子页面时只会有相应的URL改变而不会重新加载。大部分用于: 移动端 / PC端的后台管理系统单页应用的实现1.依赖hash的改变(锚点)2.依赖历史记录(history)3.前端路由:在页面中同一个位置,根据不同的hash值显示不同的页面结构实现改变URL页面不刷新按照常规的逻辑我们切换URL好像就会跳转网页,但是转念一想锚链接的URL不是也改变了吗? 这里,存在两种满足需求的方式。利用URL中的hash方式了解http协议就会知道,url的组成部分有很多,譬如协议、主机名、资源路径、查询字段等等,其中包含一个称之为片段的部分,以“#”为标识。打开控制台,输入 location.hash,你可以得到当前url的hash部分(如果当前url不存在hash则返回空字符串)。接下来,输入 location.hash = ‘123’,会发现浏览器地址栏的url变了,末尾增加了’#123’字段,并且,页面没有被重新刷新。很显然,这很符合我们的要求。利用H5的history APIhtml5引入了一个history对象,包含了一套访问浏览器历史的api,可以通过window.history访问到它。 HTML5 History API包括2个方法:history.pushState()和history.replaceState(),和1个事件:window.onpopstate。这两个方法都是对浏览器的历史栈进行操作,将传递的url和相关数据压栈,并将浏览器地址栏的url替换成传入的url且不刷新页面,而且他们的参数也相同,第一个参数用于存储该url对应的状态对象,该对象可在onpopstate事件中获取,也可在history对象中获取。第二个参数是标题,目前浏览器并未实现。第三个参数则是设定的url。一般设置为相对路径,如果设置为绝对路径时需要保证同源。。不同的是pushState 将指定的url直接压入历史记录栈顶,而 replaceState 是将当前历史记录栈顶替换成传入的数据。不过低版本对history AIP的兼容性不是很好。监听URL的变化,执行页面替换逻辑1.对于hash方式,我们通常采用监听hashchange事件,在事件回调中处理相应的页面视图展示等逻辑。两种实现的比较总的来说,基于Hash的路由,兼容性更好;基于History API的路由,更加直观和正式。但是,有一点很大的区别是,基于Hash的路由不需要对服务器做改动,基于History API的路由需要对服务器做一些改造。代码实现<div id="box">
<div class="top">顶部通栏</div>
<div class="bottom">
<div class="slide">
<a href="#/first">first</a>
<a href="#/second">second</a>
<a href="#/third">third</a>
<a href="#/fourth">fourth</a>
</div>
<div class="content router-view"></div>
</div>
</div><style>
*{
margin: 0;
padding: 0;
}
html,body,#box{
width: 100%;
height:100%;
}
#box{
display: flex;
flex-direction:column;
}
#box>.top{
height: 130px;
background: skyblue;
}
#box>.bottom{
flex:1;
display: flex;
}
#box>.bottom>.slide{
width: 230px;
background-color: orange;
box-sizing: border-box;
padding: 15px;
}
#box>.bottom>.slide>a{
font-size: 22px;
display: block;
margin: 10px 0;
text-decoration: none;
color: #333;
}
#box>.bottom>.content{
flex: 1;
box-sizing: border-box;
padding: 15px;
background: purple;
font-size: 100px;
color: white
}
</style>//1. 准备不同的页面显示结构
const template1 = `
<div>
first
</div>
`;
const template2 = `
<div>
second
</div>
`;
const template3 = `
<div>
third
</div>
`;
const template4 = `
<div>
fourth
</div>
`;
//2. 获取路由入口对象
let routerView = document.querySelector('.router-view');
//3. 捕获浏览器地址栏 hash值的改变
window.onhashchange = function(){
console.log('地址栏hash值改变了');
console.log(window.location.hash);
//获取hash
const {hash} = window.location;
//判断hash值,进行不同的结构渲染
switch(hash){
case '#/first' : routerView.innerHTML = template1;break;
case '#/second' : routerView.innerHTML = template2;break;
case '#/third' : routerView.innerHTML = template3;break;
case '#/fourth' : routerView.innerHTML = template4;break;
}
}代码升级(组件化开发)index.html<div id="box">
<div class="top">顶部通栏</div>
<div class="bottom">
<div class="slide">
<a href="#/first">first</a>
<a href="#/second">second</a>
<a href="#/third">third</a>
<a href="#/fourth">fourth</a>
</div>
<div class="content router-view"></div>
</div>
</div>*{
margin: 0;
padding: 0;
}
html,body,#box{
width: 100%;
height:100%;
}
#box{
display: flex;
flex-direction:column;
}
#box>.top{
height: 130px;
background: skyblue;
}
#box>.bottom{
flex:1;
display: flex;
}
#box>.bottom>.slide{
width: 230px;
background-color: orange;
box-sizing: border-box;
padding: 15px;
}
#box>.bottom>.slide>a{
font-size: 22px;
display: block;
margin: 10px 0;
text-decoration: none;
color: #333;
}
#box>.bottom>.content{
flex: 1;
box-sizing: border-box;
padding: 15px;
background: purple;
font-size: 100px;
color: white
}<script src="./index.js" type="module"></script>
2. components 组件1.first.js//组件的html结构
const template = `
<div id="first">
first
</div>
`;
//获取到路由出口对象
const routerView = document.querySelector('.router-view');
//准备一个渲染函数
function render(){
routerView.innerHTML = template;
}
//导出渲染函数
export default render;2.second.js//组件的html结构
const template = `
<div id="second">
second
</div>
`;
//获取到路由出口对象
const routerView = document.querySelector('.router-view');
//准备一个渲染函数
function render(){
routerView.innerHTML = template;
}
//导出渲染函数
export default render;3.third.js//组件的html结构
const template = `
<div id="third">
third
</div>
`;
//获取到路由出口对象
const routerView = document.querySelector('.router-view');
//准备一个渲染函数
function render(){
routerView.innerHTML = template;
}
//导出渲染函数
export default render;4.fourth.js//组件的html结构
const template = `
<div id="fourth">
fourth
</div>
`;
//获取到路由出口对象
const routerView = document.querySelector('.router-view');
//准备一个渲染函数
function render(){
routerView.innerHTML = template;
}
//导出渲染函数
export default render;3.router.js//导入组件模块
import firstCom from './components/first.js';
import secondCom from './components/second.js';
import thirdCom from './components/third.js';
import fourthCom from './components/fourth.js';
// 定义路由表
const router = [
{
name : '/first',
component: firstCom
},
{
name: '/second',
component: secondCom
},
{
name: '/third',
component: thirdCom
},
{
name: '/fourth',
component: fourthCom
}
];
//导出路由
export default router;index.js//1. 导入路由表
import router from './router.js';
//2. 注册hashchange 事件
window.onhashchange = hashChange;
hashChange();
//3. 处理程序
function hashChange(){
console.log('根据当前hash值,进行改变');
//获取当前的hash值
const hash = window.location.hash.slice(1) || '/';
// console.log(hash);
//从路由表中找到hash对应的那一个信息
const info = router.find(item => item.name === hash);
console.log(info);
//如果没有内容,不需要后续操作
if(!info){
return;
}
//渲染页面
info.component();
}
Ned
JavaScript深浅拷贝解析:探索对象和数组复制的细节与技巧
对象详解Object.defineProperty1.Object.defineProperty(obj, prop, descriptor) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。obj : 要定义属性的对象。prop : 要定义或修改的属性的名称。descriptor : 要定义或修改的属性描述符。2.对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。3.数据描述符:1.configurable:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性,默认值为false。var obj = {
name : "张三"
}
Object.defineProperty(obj,"name",{
configurable : false
})
console.log(obj); //{name : "张三"}
delete obj.name;
console.log(obj); //{name : "张三"}2.enumerable:表示能否通过for in循环访问属性,默认值为false3.writable:表示能否修改属性的值。默认值为false。var obj = {
name : "张三"
}
Object.defineProperty(obj,'age',{
writable : false,
value : 18
})
console.log(obj.age); //18
obj.age = 20;
console.log(obj.age); //184.value:包含这个属性的数据值。默认值为undefined。4.存取描述符get:getter在读取属性时调用的函数,默认值是undefinedset:setter在写入属性的时候调用的函数,默认值是undefined注 : setter 不能和writable 、value 一起使用。var obj = {
_year : 2022
}
Object.defineProperty(obj,'year',{
get : function(){
return this._year;
},
set : function(yyyy){
if(yyyy > 2022){
this._year = yyyy;
}
}
})
obj.year = 2023;
console.log(obj._year);1.定义多个属性var student = {};
var obj = {};
Object.defineProperties(student,{
name: {
writeble : false,
value: "张三"
},
age : {
writeble: true,
value: 16
},
sex: {
get(){
return '男';
},
set(v){
obj.sex = v;
}
}
})
obj.sex = '男';
console.log(student.name + ':' + student.age); //张三:16
console.log(obj.sex); //男
student.sex = '女';
console.log(student.sex); //男
console.log(obj.sex); //女Proxy概念:Proxy : ES6提供的数据代理,表示由它来“代理”某些操作,可以称为“代理器"2.语法:new Proxy(代理原始对象,{配置项}) : 返回的实例对象,就是代理结果数据new Proxy()表示生成一个Proxy实例代理原始对象: 要被代理的对象,可以是一个object或者function配置项:也是一个对象,对该代理对象的各种操作行为处理。Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写let obj = {name: '张三',age: 18};
//开始代理
let result = new Proxy(obj,{
//配置get来进行代理设置
get(target,property){
//target: 就是你要代理的目标对象,我们当前是obj
//property: 就是该对象内的每一个属性,自动遍历
return target[property];
},
//配置set来进行修改
set(target,property,val){
//target: 就是你要代理的目标对象,我们当前是obj
//property: 就是该对象内的你要修改的那个属性
//val: 就是你要修改的那个属性的值
target[property] = val;
console.log('你在修改' + property + '属性,你想修改为:' + val);
//注意:简单代理需要返回 true
return true;
}
})
console.log('原始数据:' + obj);
console.log('代理结果:' + result);
console.log('代理结果 name : ' + result.name);
//尝试修改
result.name = '李四';
console.log('代理结果 name: ' + result .name);
//动态插入
result.sex = '男';
console.log('代理结果 sex:' + result.sex);hasOwnPropertyhasOwnProperty : 表示是否有自己的属性。这个方法会查找一个对象是否有某个属性,但是不会去查找它的原型链。let obj = {name: '张三',age: 18};
//开始代理
let result = new Proxy(obj,{
//配置get来进行代理设置
get(target,property){
//target: 就是你要代理的目标对象,我们当前是obj
//property: 就是该对象内的每一个属性,自动遍历
return target[property];
},
//配置set来进行修改
set(target,property,val){
//target: 就是你要代理的目标对象,我们当前是obj
//property: 就是该对象内的你要修改的那个属性
//val: 就是你要修改的那个属性的值
target[property] = val;
console.log('你在修改' + property + '属性,你想修改为:' + val);
//注意:简单代理需要返回 true
return true;
}
})
console.log('原始数据:' + obj);
console.log('代理结果:' + result);
console.log('代理结果 name : ' + result.name);
//尝试修改
result.name = '李四';
console.log('代理结果 name: ' + result .name);
//动态插入
result.sex = '男';
console.log('代理结果 sex:' + result.sex);判断自身属性是否存在let obj = new Object(); //创建一个空对象
obj.name = '张三'; //添加一个name属性
//备份一份旧的name属性,然后删除旧属性
function changeObj(){
obj.newname = obj.name;
delete obj.name;
}
console.log(obj.hasOwnProperty('name')); //true
changeObj();
console.log(obj.hasOwnProperty('name')); //false判断自身属性与继承属性function foo() {
this.name = 'foo'
this.sayHi = function () {
console.log('Say Hi')
}
}
foo.prototype.sayGoodBy = function () {
console.log('Say Good By')
}
let myPro = new foo()
console.log(myPro.name) // foo
console.log(myPro.hasOwnProperty('name')) // true
console.log(myPro.hasOwnProperty('toString')) // false
console.log(myPro.hasOwnProperty('hasOwnProperty')) // false
console.log(myPro.hasOwnProperty('sayHi')) // true
console.log(myPro.hasOwnProperty('sayGoodBy')) // false
console.log('sayGoodBy' in myPro) // true遍历一个对象的所有自身属性:使用hasOwnProperty()方法来忽略继承属性。var buz = {
a: 1
};
for (var name in buz) {
if (buz.hasOwnProperty(name)) {
alert(name + ':' + buz[name]);
}
else {
alert(name);
}
}深浅拷贝针对引用类型而言,浅拷贝指的是复制对象的引用,即直接给引用类型赋值,如果拷贝后的对象发生变化,原对象也会发生变化。而深拷贝是真正地对对象进行拷贝,修改拷贝后的新对象并不会对原对象产生任何影响。在JS中数据类型分为基本类型和引用类型 基本类型: number, boolean,string,symbol,undefined,null引用类型:object 以及一些标准内置对象 Array、RegExp、String、Map、Set..基本类型数据拷贝基本类型数据都是值类型,存储在栈内存中,每次赋值都是一次复制的过程let obj = {
a : 1,
b : {
c : 2,
d : {
f : 3
}
}
}
let newObj = {... obj};
console.log(newObj);
obj.b.c = 10;
console.log(newObj);
function cloneObj(obj){
let clone = {};
for(let key in obj){
clone[key] = obj[key];
}
return clone;
}
let a = {
x : {
y : 1
}
}
let b = cloneObj(a);
console.log(a,b);引用类型数据拷贝只拷贝对象的一层数据,再深处层次的引用类型value将只会拷贝引用浅拷贝let obj = {
a : 1,
b : {
c : 2,
d : {
f : 3
}
}
}
let newObj = {... obj};
console.log(newObj);
obj.b.c = 10;
console.log(newObj);
function cloneObj(obj){
let clone = {};
for(let key in obj){
clone[key] = obj[key];
}
return clone;
}
let a = {
x : {
y : 1
}
}
let b = cloneObj(a);
console.log(a,b);深拷贝深拷贝就不会像浅拷贝那样只拷贝一层,而是有多少层我就拷贝多少层,要真正的做到全部内容都放在自己新开辟的内存里。可以利用递归思想实现深拷贝。function cloneObj(obj) {
let clone = {};
for (let i in obj) {
// 如果为对象则递归更进一层去拷贝
if (typeof obj[i] == "object" && obj[i] != null) {
clone[i] = cloneObj(obj[i]);
} else {
clone[i] = obj[i];
}
}
return clone;
}一、数据类型存储前面文章我们讲到,JavaScript中存在两大数据类型:基本类型引用类型基本类型数据保存在在栈内存中引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中二、浅拷贝浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址下面简单实现一个浅拷贝function shallowClone(obj) {
const newObj = {};
for(let prop in obj) {
if(obj.hasOwnProperty(prop)){
newObj[prop] = obj[prop];
}
}
return newObj;
}在JavaScript中,存在浅拷贝的现象有:Object.assignvar obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'fx',
name2: 'xka'
},
love: function () {
console.log('fx is a great girl')
}
}
var newObj = Object.assign({}, fxObj);slice()const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.slice(0)
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]concat()const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]拓展运算符const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]三、深拷贝深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性常见的深拷贝方式有:_.cloneDeep()jQuery.extend()JSON.stringify()手写循环递归_.cloneDeep()const _ = require('lodash');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// falsejQuery.extend()const $ = require('jquery');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // falseJSON.stringify()const obj2=JSON.parse(JSON.stringify(obj1));但是这种方式存在弊端,会忽略undefined、symbol和函数const obj = {
name: 'A',
name1: undefined,
name3: function() {},
name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}循环递归function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}四、区别下面首先借助两张图,可以更加清晰看到浅拷贝与深拷贝的区别从上图发现,浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象// 深拷贝
const obj1 = {
name : 'init',
arr : [1,[2,3],4],
};
const obj4=deepClone(obj1) // 一个深拷贝方法
obj4.name = "update";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存
console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象// 深拷贝
const obj1 = {
name : 'init',
arr : [1,[2,3],4],
};
const obj4=deepClone(obj1) // 一个深拷贝方法
obj4.name = "update";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存
console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
Ned
最全面的 JavaScript 基础代码手写指南,读完这篇就够了!
一、JavaScript 基础1. 手写 Object.create思路:将传入的对象作为原型function create(obj) {
function F() {}
F.prototype = obj
return new F()
}2. 手写 instanceof 方法instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。实现步骤:首先获取类型的原型然后获得对象的原型然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null具体实现:function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left), // 获取对象的原型
prototype = right.prototype; // 获取构造函数的 prototype 对象
// 判断构造函数的 prototype 对象是否在对象的原型链上
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}3. 手写 new 操作符在调用 new 的过程中会发生以上四件事情:(1)首先创建了一个新的空对象(2)设置原型,将对象的原型设置为函数的 prototype 对象。(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。function objectFactory() {
let newObject = null;
let constructor = Array.prototype.shift.call(arguments);
let result = null;
// 判断参数是否是一个函数
if (typeof constructor !== "function") {
console.error("type error");
return;
}
// 新建一个空对象,对象的原型为构造函数的 prototype 对象
newObject = Object.create(constructor.prototype);
// 将 this 指向新建对象,并执行函数
result = constructor.apply(newObject, arguments);
// 判断返回对象
let flag = result && (typeof result === "object" || typeof result === "function");
// 判断返回结果
return flag ? result : newObject;
}
// 使用方法
objectFactory(构造函数, 初始化参数);4. 手写 Promiseconst PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
function MyPromise(fn) {
// 保存初始化状态
var self = this;
// 初始化状态
this.state = PENDING;
// 用于保存 resolve 或者 rejected 传入的值
this.value = null;
// 用于保存 resolve 的回调函数
this.resolvedCallbacks = [];
// 用于保存 reject 的回调函数
this.rejectedCallbacks = [];
// 状态转变为 resolved 方法
function resolve(value) {
// 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变
if (value instanceof MyPromise) {
return value.then(resolve, reject);
}
// 保证代码的执行顺序为本轮事件循环的末尾
setTimeout(() => {
// 只有状态为 pending 时才能转变,
if (self.state === PENDING) {
// 修改状态
self.state = RESOLVED;
// 设置传入的值
self.value = value;
// 执行回调函数
self.resolvedCallbacks.forEach(callback => {
callback(value);
});
}
}, 0);
}
// 状态转变为 rejected 方法
function reject(value) {
// 保证代码的执行顺序为本轮事件循环的末尾
setTimeout(() => {
// 只有状态为 pending 时才能转变
if (self.state === PENDING) {
// 修改状态
self.state = REJECTED;
// 设置传入的值
self.value = value;
// 执行回调函数
self.rejectedCallbacks.forEach(callback => {
callback(value);
});
}
}, 0);
}
// 将两个方法传入函数执行
try {
fn(resolve, reject);
} catch (e) {
// 遇到错误时,捕获错误,执行 reject 函数
reject(e);
}
}
MyPromise.prototype.then = function(onResolved, onRejected) {
// 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
onResolved =
typeof onResolved === "function"
? onResolved
: function(value) {
return value;
};
onRejected =
typeof onRejected === "function"
? onRejected
: function(error) {
throw error;
};
// 如果是等待状态,则将函数加入对应列表中
if (this.state === PENDING) {
this.resolvedCallbacks.push(onResolved);
this.rejectedCallbacks.push(onRejected);
}
// 如果状态已经凝固,则直接执行对应状态的函数
if (this.state === RESOLVED) {
onResolved(this.value);
}
if (this.state === REJECTED) {
onRejected(this.value);
}
};5. 手写 Promise.thenthen 方法返回一个新的 promise 实例,为了在 promise 状态发生变化时(resolve / reject 被调用时)再执行 then 里的函数,我们使用一个 callbacks 数组先把传给then的函数暂存起来,等状态改变时再调用。那么,怎么保证后一个 *\*\*then\*\** 里的方法在前一个 *\*\*then\*\**(可能是异步)结束之后再执行呢? 我们可以将传给 then 的函数和新 promise 的 resolve 一起 push 到前一个 promise 的 callbacks 数组中,达到承前启后的效果:承前:当前一个 promise 完成后,调用其 resolve 变更状态,在这个 resolve 里会依次调用 callbacks 里的回调,这样就执行了 then 里的方法了启后:上一步中,当 then 里的方法执行完成后,返回一个结果,如果这个结果是个简单的值,就直接调用新 promise 的 resolve,让其状态变更,这又会依次调用新 promise 的 callbacks 数组里的方法,循环往复。。如果返回的结果是个 promise,则需要等它完成之后再触发新 promise 的 resolve,所以可以在其结果的 then 里调用新 promise 的 resolvethen(onFulfilled, onReject){
// 保存前一个promise的this
const self = this;
return new MyPromise((resolve, reject) => {
// 封装前一个promise成功时执行的函数
let fulfilled = () => {
try{
const result = onFulfilled(self.value); // 承前
return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); //启后
}catch(err){
reject(err)
}
}
// 封装前一个promise失败时执行的函数
let rejected = () => {
try{
const result = onReject(self.reason);
return result instanceof MyPromise? result.then(resolve, reject) : reject(result);
}catch(err){
reject(err)
}
}
switch(self.status){
case PENDING:
self.onFulfilledCallbacks.push(fulfilled);
self.onRejectedCallbacks.push(rejected);
break;
case FULFILLED:
fulfilled();
break;
case REJECT:
rejected();
break;
}
})
}注意:连续多个 then 里的回调方法是同步注册的,但注册到了不同的 callbacks 数组中,因为每次 then 都返回新的 promise 实例(参考上面的例子和图)注册完成后开始执行构造函数中的异步事件,异步完成之后依次调用 callbacks 数组中提前注册的回调6. 手写 Promise.all1) 核心思路接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数这个方法返回一个新的 promise 对象,遍历传入的参数,用Promise.resolve()将参数"包一层",使其变成一个promise对象参数所有回调成功才是成功,返回值数组与参数顺序一致参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。2)实现代码一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了function promiseAll(promises) {
return new Promise(function(resolve, reject) {
if(!Array.isArray(promises)){
throw new TypeError(`argument must be a array`)
}
var resolvedCounter = 0;
var promiseNum = promises.length;
var resolvedResult = [];
for (let i = 0; i < promiseNum; i++) {
Promise.resolve(promises[i]).then(value=>{
resolvedCounter++;
resolvedResult[i] = value;
if (resolvedCounter == promiseNum) {
return resolve(resolvedResult)
}
},error=>{
return reject(error)
})
}
})
}
// test
let p1 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(1)
}, 1000)
})
let p2 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(2)
}, 2000)
})
let p3 = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(3)
}, 3000)
})
promiseAll([p3, p1, p2]).then(res => {
console.log(res) // [3, 1, 2]
})7. 手写 Promise.race该方法的参数是 Promise 实例数组, 然后其 then 注册的回调方法是数组中的某一个 Promise 的状态变为 fulfilled 的时候就执行. 因为 Promise 的状态只能改变一次, 那么我们只需要把 Promise.race 中产生的 Promise 对象的 resolve 方法, 注入到数组中的每一个 Promise 实例中的回调函数中即可.Promise.race = function (args) {
return new Promise((resolve, reject) => {
for (let i = 0, len = args.length; i < len; i++) {
args[i].then(resolve, reject)
}
})
}8. 手写防抖函数函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。// 函数防抖的实现,fn是高频触发的函数,delay
function debounce(fn, wait) {
let timer = null;
return function() {
let context = this,
args = arguments;
// 如果此时存在定时器的话,则取消之前的定时器重新记时
if (timer) {
clearTimeout(timer);
timer = null;
}
// 设置定时器,使事件间隔指定事件后执行
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
}9. 手写节流函数函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。// 函数节流的实现;
function throttle(fn, delay) {
let curTime = Date.now();
return function() {
let context = this,
args = arguments,
nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。
if (nowTime - curTime >= delay) {
curTime = Date.now();
return fn.apply(context, args);
}
};
}10. 手写类型判断函数function getType(value) {
// 判断数据是 null 的情况
if (value === null) {
return value + "";
}
// 判断数据是引用类型的情况
if (typeof value === "object") {
let valueClass = Object.prototype.toString.call(value),
type = valueClass.split(" ")[1].split("");
type.pop();
return type.join("").toLowerCase();
} else {
// 判断数据是基本数据类型的情况和函数的情况
return typeof value;
}
}11. 手写 call 函数call 函数的实现步骤:判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。判断传入上下文对象是否存在,如果不存在,则设置为 window 。处理传入的参数,截取第一个参数后的所有参数。将函数作为上下文对象的一个属性。使用上下文对象来调用这个方法,并保存返回结果。删除刚才新增的属性。返回结果。// call函数实现
Function.prototype.myCall = function(context) {
// 判断调用对象
if (typeof this !== "function") {
console.error("type error");
}
// 获取参数
let args = [...arguments].slice(1),
result = null;
// 判断 context 是否传入,如果未传入则设置为 window
context = context || window;
// 将调用函数设为对象的方法
context.fn = this;
// 调用函数
result = context.fn(...args);
// 将属性删除
delete context.fn;
return result;
};12. 手写 apply 函数apply 函数的实现步骤:判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。判断传入上下文对象是否存在,如果不存在,则设置为 window 。将函数作为上下文对象的一个属性。判断参数值是否传入使用上下文对象来调用这个方法,并保存返回结果。删除刚才新增的属性返回结果// apply 函数实现
Function.prototype.myApply = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
let result = null;
// 判断 context 是否存在,如果未传入则为 window
context = context || window;
// 将函数设为对象的方法
context.fn = this;
// 调用方法
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
// 将属性删除
delete context.fn;
return result;
};13. 手写 bind 函数bind 函数的实现步骤:判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。保存当前函数的引用,获取其余传入参数值。创建一个函数返回函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。// bind 函数实现
Function.prototype.myBind = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 获取参数
var args = [...arguments].slice(1),
fn = this;
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
);
};
};14. 函数柯里化的实现函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。function curry(fn, args) {
// 获取函数需要的参数长度
let length = fn.length;
args = args || [];
return function() {
let subArgs = args.slice(0);
// 拼接得到现有的所有参数
for (let i = 0; i < arguments.length; i++) {
subArgs.push(arguments[i]);
}
// 判断参数的长度是否已经满足函数所需参数的长度
if (subArgs.length >= length) {
// 如果满足,执行函数
return fn.apply(this, subArgs);
} else {
// 如果不满足,递归返回科里化的函数,等待参数的传入
return curry.call(this, fn, subArgs);
}
};
}
// es6 实现
function curry(fn, ...args) {
return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}15. 实现AJAX请求AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。创建AJAX请求的步骤:创建一个 XMLHttpRequest 对象。在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);16. 使用Promise封装AJAX请求// promise 封装实现:
function getJSON(url) {
// 创建一个 promise 对象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// 新建一个 http 请求
xhr.open("GET", url, true);
// 设置状态的监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功或失败时,改变 promise 的状态
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
// 设置错误监听函数
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 设置响应的数据类型
xhr.responseType = "json";
// 设置请求头信息
xhr.setRequestHeader("Accept", "application/json");
// 发送 http 请求
xhr.send(null);
});
return promise;
}17. 实现浅拷贝浅拷贝是指,一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值,如果是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。(1)Object.assign()Object.assign()是ES6中对象的拷贝方法,接受的第一个参数是目标对象,其余参数是源对象,用法:Object.assign(target, source_1, ···),该方法可以实现浅拷贝,也可以实现一维对象的深拷贝。注意:如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回。因为null 和 undefined 不能转化为对象,所以第一个参数不能为null或 undefined,会报错。let target = {a: 1};
let object2 = {b: 2};
let object3 = {c: 3};
Object.assign(target,object2,object3);
console.log(target); // {a: 1, b: 2, c: 3}(2)扩展运算符使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。语法:let cloneObj = { ...obj };let obj1 = {a:1,b:{c:1}}
let obj2 = {...obj1};
obj1.a = 2;
console.log(obj1); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj1.b.c = 2;
console.log(obj1); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}(3)数组方法实现数组浅拷贝1)Array.prototype.sliceslice()方法是JavaScript数组的一个方法,这个方法可以从已有数组中返回选定的元素:用法:array.slice(start, end),该方法不会改变原始数组。该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。let arr = [1,2,3,4];
console.log(arr.slice()); // [1,2,3,4]
console.log(arr.slice() === arr); //false2)Array.prototype.concatconcat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。let arr = [1,2,3,4];
console.log(arr.concat()); // [1,2,3,4]
console.log(arr.concat() === arr); //false(4)手写实现浅拷贝// 浅拷贝的实现;
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 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 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;
}18. 实现深拷贝浅拷贝: 浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用 Object.assign 和展开运算符来实现。深拷贝: 深拷贝相对浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用 JSON 的两个函数来实现,但是由于 JSON 的对象格式比 js 的对象格式更加严格,所以如果属性值里边出现函数或者 Symbol 类型的值时,会转换失败(1)JSON.stringify()JSON.parse(JSON.stringify(obj))是目前比较常用的深拷贝方法之一,它的原理就是利用JSON.stringify 将js对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象。这个方法可以简单粗暴的实现深拷贝,但是还存在问题,拷贝的对象中如果有函数,undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。let obj1 = { a: 0,
b: {
c: 0
}
};
let obj2 = JSON.parse(JSON.stringify(obj1));
obj1.a = 1;
obj1.b.c = 1;
console.log(obj1); // {a: 1, b: {c: 1}}
console.log(obj2); // {a: 0, b: {c: 0}}(2)函数库lodash的_.cloneDeep方法该函数库也有提供_.cloneDeep用来做 Deep Copyvar _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false(3)手写实现深拷贝函数// 深拷贝的实现
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;
}
Ned
精通 JavaScript 数据处理大全:手写代码从入门到精通
1. 实现日期格式化函数输入:dateFormat(new Date('2020-12-01'), 'yyyy/MM/dd') // 2020/12/01
dateFormat(new Date('2020-04-01'), 'yyyy/MM/dd') // 2020/04/01
dateFormat(new Date('2020-04-01'), 'yyyy年MM月dd日') // 2020年04月01日
复制代码
const dateFormat = (dateInput, format)=>{
var day = dateInput.getDate()
var month = dateInput.getMonth() + 1
var year = dateInput.getFullYear()
format = format.replace(/yyyy/, year)
format = format.replace(/MM/,month)
format = format.replace(/dd/,day)
return format
}
复制代码2. 交换a,b的值,不能用临时变量巧妙的利用两个数的和、差:a = a + b
b = a - b
a = a - b
复制代码3. 实现数组的乱序输出主要的实现思路就是:取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行交换。第二次取出数据数组第二个元素,随机产生一个除了索引为1的之外的索引值,并将第二个元素与该索引值对应的元素进行交换按照上面的规律执行,直到遍历完成a = a + b
b = a - b
a = a - b
复制代码还有一方法就是倒序遍历:var arr = [1,2,3,4,5,6,7,8,9,10];
let length = arr.length,
randomIndex,
temp;
while (length) {
randomIndex = Math.floor(Math.random() * length--);
temp = arr[length];
arr[length] = arr[randomIndex];
arr[randomIndex] = temp;
}
console.log(arr)
复制代码4. 实现数组元素求和arr=[1,2,3,4,5,6,7,8,9,10],求和let arr=[1,2,3,4,5,6,7,8,9,10]
let sum = arr.reduce( (total,i) => total += i,0);
console.log(sum);
复制代码arr=[1,2,3,[[4,5],6],7,8,9],求和var = arr=[1,2,3,[[4,5],6],7,8,9]
let arr= arr.toString().split(',').reduce( (total,i) => total += Number(i),0);
console.log(arr);
复制代码递归实现:let arr = [1, 2, 3, 4, 5, 6]
function add(arr) {
if (arr.length == 1) return arr[0]
return arr[0] + add(arr.slice(1))
}
console.log(add(arr)) // 21
复制代码5. 实现数组的扁平化(1)递归实现普通的递归思路很容易理解,就是通过循环递归的方式,一项一项地去遍历,如果每一项还是一个数组,那么就继续往下遍历,利用递归程序的方法,来实现数组的每一项的连接:let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
let result = [];
for(let i = 0; i < arr.length; i++) {
if(Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]));
} else {
result.push(arr[i]);
}
}
return result;
}
flatten(arr); // [1, 2, 3, 4,5]
复制代码(2)reduce 函数迭代从上面普通的递归函数中可以看出,其实就是对数组的每一项进行处理,那么其实也可以用reduce 来实现数组的拼接,从而简化第一种方法的代码,改造后的代码如下所示:let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.reduce(function(prev, next){
return prev.concat(Array.isArray(next) ? flatten(next) : next)
}, [])
}
console.log(flatten(arr));// [1, 2, 3, 4,5]
复制代码(3)扩展运算符实现这个方法的实现,采用了扩展运算符和 some 的方法,两者共同使用,达到数组扁平化的目的:let arr = [1, [2, [3, 4]]];
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
复制代码(4)split 和 toString可以通过 split 和 toString 两个方法来共同实现数组扁平化,由于数组会默认带一个 toString 的方法,所以可以把数组直接转换成逗号分隔的字符串,然后再用 split 方法把字符串重新转换为数组,如下面的代码所示:let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.toString().split(',');
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
复制代码通过这两个方法可以将多维数组直接转换成逗号连接的字符串,然后再重新分隔成数组。(5)ES6 中的 flat我们还可以直接调用 ES6 中的 flat 方法来实现数组扁平化。flat 方法的语法:arr.flat([depth])其中 depth 是 flat 的参数,depth 是可以传递数组的展开深度(默认不填、数值是 1),即展开一层数组。如果层数不确定,参数可以传进 Infinity,代表不论多少层都要展开:let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.flat(Infinity);
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
复制代码可以看出,一个嵌套了两层的数组,通过将 flat 方法的参数设置为 Infinity,达到了我们预期的效果。其实同样也可以设置成 2,也能实现这样的效果。在编程过程中,如果数组的嵌套层数不确定,最好直接使用 Infinity,可以达到扁平化。 (6)正则和 JSON 方法 在第4种方法中已经使用 toString 方法,其中仍然采用了将 JSON.stringify 的方法先转换为字符串,然后通过正则表达式过滤掉字符串中的数组的方括号,最后再利用 JSON.parse 把它转换成数组:let arr = [1, [2, [3, [4, 5]]], 6];
function flatten(arr) {
let str = JSON.stringify(arr);
str = str.replace(/(\[|\])/g, '');
str = '[' + str + ']';
return JSON.parse(str);
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
复制代码6. 实现数组去重给定某无序数组,要求去除数组中的重复数字并且返回新的无重复数组。ES6方法(使用数据结构集合):const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]
复制代码ES5方法:使用map存储不重复的数字const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
uniqueArray(array); // [1, 2, 3, 5, 9, 8]
function uniqueArray(array) {
let map = {};
let res = [];
for(var i = 0; i < array.length; i++) {
if(!map.hasOwnProperty([array[i]])) {
map[array[i]] = 1;
res.push(array[i]);
}
}
return res;
}
复制代码7. 实现数组的flat方法function _flat(arr, depth) {
if(!Array.isArray(arr) || depth <= 0) {
return arr;
}
return arr.reduce((prev, cur) => {
if (Array.isArray(cur)) {
return prev.concat(_flat(cur, depth - 1))
} else {
return prev.concat(cur);
}
}, []);
}
复制代码8. 实现数组的push方法let arr = [];
Array.prototype.push = function() {
for( let i = 0 ; i < arguments.length ; i++){
this[this.length] = arguments[i] ;
}
return this.length;
}
复制代码9. 实现数组的filter方法Array.prototype._filter = function(fn) {
if (typeof fn !== "function") {
throw Error('参数必须是一个函数');
}
const res = [];
for (let i = 0, len = this.length; i < len; i++) {
fn(this[i]) && res.push(this[i]);
}
return res;
}
复制代码10. 实现数组的map方法Array.prototype._map = function(fn) {
if (typeof fn !== "function") {
throw Error('参数必须是一个函数');
}
const res = [];
for (let i = 0, len = this.length; i < len; i++) {
res.push(fn(this[i]));
}
return res;
}
复制代码11. 实现字符串的repeat方法输入字符串s,以及其重复的次数,输出重复的结果,例如输入abc,2,输出abcabc。function repeat(s, n) {
return (new Array(n + 1)).join(s);
}
复制代码递归:function repeat(s, n) {
return (n > 0) ? s.concat(repeat(s, --n)) : "";
}
复制代码12. 实现字符串翻转在字符串的原型链上添加一个方法,实现字符串翻转:String.prototype._reverse = function(a){
return a.split("").reverse().join("");
}
var obj = new String();
var res = obj._reverse ('hello');
console.log(res); // olleh
复制代码需要注意的是,必须通过实例化对象之后再去调用定义的方法,不然找不到该方法。13. 将数字每千分位用逗号隔开数字有小数版本:let format = n => {
let num = n.toString() // 转成字符串
let decimals = ''
// 判断是否有小数
num.indexOf('.') > -1 ? decimals = num.split('.')[1] : decimals
let len = num.length
if (len <= 3) {
return num
} else {
let temp = ''
let remainder = len % 3
decimals ? temp = '.' + decimals : temp
if (remainder > 0) { // 不是3的整数倍
return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') + temp
} else { // 是3的整数倍
return num.slice(0, len).match(/\d{3}/g).join(',') + temp
}
}
}
format(12323.33) // '12,323.33'
复制代码数字无小数版本:let format = n => {
let num = n.toString()
let len = num.length
if (len <= 3) {
return num
} else {
let remainder = len % 3
if (remainder > 0) { // 不是3的整数倍
return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',')
} else { // 是3的整数倍
return num.slice(0, len).match(/\d{3}/g).join(',')
}
}
}
format(1232323) // '1,232,323'
复制代码14. 实现非负大整数相加JavaScript对数值有范围的限制,限制如下:Number.MAX_VALUE // 1.7976931348623157e+308
Number.MAX_SAFE_INTEGER // 9007199254740991
Number.MIN_VALUE // 5e-324
Number.MIN_SAFE_INTEGER // -9007199254740991
复制代码如果想要对一个超大的整数(> Number.MAX_SAFE_INTEGER)进行加法运算,但是又想输出一般形式,那么使用 + 是无法达到的,一旦数字超过 Number.MAX_SAFE_INTEGER 数字会被立即转换为科学计数法,并且数字精度相比以前将会有误差。实现一个算法进行大数的相加:function sumBigNumber(a, b) {
let res = '';
let temp = 0;
a = a.split('');
b = b.split('');
while (a.length || b.length || temp) {
temp += ~~a.pop() + ~~b.pop();
res = (temp % 10) + res;
temp = temp > 9
}
return res.replace(/^0+/, '');
}
复制代码其主要的思路如下:首先用字符串的方式来保存大数,这样数字在数学表示上就不会发生变化初始化res,temp来保存中间的计算结果,并将两个字符串转化为数组,以便进行每一位的加法运算将两个数组的对应的位进行相加,两个数相加的结果可能大于10,所以可能要仅为,对10进行取余操作,将结果保存在当前位判断当前位是否大于9,也就是是否会进位,若是则将temp赋值为true,因为在加法运算中,true会自动隐式转化为1,以便于下一次相加重复上述操作,直至计算结束13. 实现 add(1)(2)(3)函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。1)粗暴版function add (a) {
return function (b) {
return function (c) {
return a + b + c;
}
}
}
console.log(add(1)(2)(3)); // 6
复制代码2)柯里化解决方案参数长度固定var add = function (m) {
var temp = function (n) {
return add(m + n);
}
temp.toString = function () {
return m;
}
return temp;
};
console.log(add(3)(4)(5)); // 12
console.log(add(3)(6)(9)(25)); // 43
复制代码对于add(3)(4)(5),其执行过程如下:先执行add(3),此时m=3,并且返回temp函数;执行temp(4),这个函数内执行add(m+n),n是此次传进来的数值4,m值还是上一步中的3,所以add(m+n)=add(3+4)=add(7),此时m=7,并且返回temp函数执行temp(5),这个函数内执行add(m+n),n是此次传进来的数值5,m值还是上一步中的7,所以add(m+n)=add(7+5)=add(12),此时m=12,并且返回temp函数由于后面没有传入参数,等于返回的temp函数不被执行而是打印,了解JS的朋友都知道对象的toString是修改对象转换字符串的方法,因此代码中temp函数的toString函数return m值,而m值是最后一步执行函数时的值m=12,所以返回值是12。参数长度不固定function add (...args) {
//求和
return args.reduce((a, b) => a + b)
}
function currying (fn) {
let args = []
return function temp (...newArgs) {
if (newArgs.length) {
args = [
...args,
...newArgs
]
return temp
} else {
let val = fn.apply(this, args)
args = [] //保证再次调用时清空
return val
}
}
}
let addCurry = currying(add)
console.log(addCurry(1)(2)(3)(4, 5)()) //15
console.log(addCurry(1)(2)(3, 4, 5)()) //15
console.log(addCurry(1)(2, 3, 4, 5)()) //15
复制代码14. 实现类数组转化为数组类数组转换为数组的方法有这样几种:通过 call 调用数组的 slice 方法来实现转换Array.prototype.slice.call(arrayLike);
复制代码通过 call 调用数组的 splice 方法来实现转换Array.prototype.splice.call(arrayLike, 0);
复制代码通过 apply 调用数组的 concat 方法来实现转换Array.prototype.concat.apply([], arrayLike);
复制代码通过 Array.from 方法来实现转换Array.from(arrayLike);
复制代码15. 使用 reduce 求和arr = [1,2,3,4,5,6,7,8,9,10],求和let arr = [1,2,3,4,5,6,7,8,9,10]
arr.reduce((prev, cur) => { return prev + cur }, 0)
复制代码arr = [1,2,3,[[4,5],6],7,8,9],求和let arr = [1,2,3,4,5,6,7,8,9,10]
arr.flat(Infinity).reduce((prev, cur) => { return prev + cur }, 0)
复制代码arr = [{a:1, b:3}, {a:2, b:3, c:4}, {a:3}],求和let arr = [{a:9, b:3, c:4}, {a:1, b:3}, {a:3}]
arr.reduce((prev, cur) => {
return prev + cur["a"];
}, 0)
复制代码16. 将js对象转化为树形结构// 转换前:
source = [{
id: 1,
pid: 0,
name: 'body'
}, {
id: 2,
pid: 1,
name: 'title'
}, {
id: 3,
pid: 2,
name: 'div'
}]
// 转换为:
tree = [{
id: 1,
pid: 0,
name: 'body',
children: [{
id: 2,
pid: 1,
name: 'title',
children: [{
id: 3,
pid: 1,
name: 'div'
}]
}
}]
复制代码代码实现:function jsonToTree(data) {
// 初始化结果数组,并判断输入数据的格式
let result = []
if(!Array.isArray(data)) {
return result
}
// 使用map,将当前对象的id与当前对象对应存储起来
let map = {};
data.forEach(item => {
map[item.id] = item;
});
//
data.forEach(item => {
let parent = map[item.pid];
if(parent) {
(parent.children || (parent.children = [])).push(item);
} else {
result.push(item);
}
});
return result;
}
复制代码17. 使用ES5和ES6求函数参数的和ES5:function sum() {
let sum = 0
Array.prototype.forEach.call(arguments, function(item) {
sum += item * 1
})
return sum
}
复制代码ES6:function sum(...nums) {
let sum = 0
nums.forEach(function(item) {
sum += item * 1
})
return sum
}
复制代码18. 解析 URL Params 为对象let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 结果
{ user: 'anonymous',
id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
city: '北京', // 中文需解码
enabled: true, // 未指定值得 key 约定为 true
}
*/
复制代码
function parseParam(url) {
const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中
let paramsObj = {};
// 将 params 存到对象中
paramsArr.forEach(param => {
if (/=/.test(param)) { // 处理有 value 的参数
let [key, val] = param.split('='); // 分割 key 和 value
val = decodeURIComponent(val); // 解码
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创建 key 并设置值
paramsObj[key] = val;
}
} else { // 处理没有 value 的参数
paramsObj[param] = true;
}
})
return paramsObj;
}
复制代码
Ned
JavaScript 中的双等号(==)和三等号(===)有何不同?何时使用它们?
== 和 ===区别,分别在什么情况使用一、等于操作符等于操作符用两个等于号( == )表示,如果操作数相等,则会返回 true前面文章,我们提到在JavaScript中存在隐式转换。等于操作符(==)在比较中会先进行类型转换,再确定操作数是否相等遵循以下规则:如果任一操作数是布尔值,则将其转换为数值再比较是否相等let result1 = (true == 1); // true如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等let result1 = ("55" == 55); // true如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()方法取得其原始值,再根据前面的规则进行比较let obj = {valueOf:function(){return 1}}
let result1 = (obj == 1); // truenull和undefined相等let result1 = (null == undefined ); // true如果有任一操作数是 NaN ,则相等操作符返回 falselet result1 = (NaN == NaN ); // false如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回truelet obj1 = {name:"xxx"}
let obj2 = {name:"xxx"}
let result1 = (obj1 == obj2 ); // false下面进一步做个小结:两个都为简单类型,字符串和布尔值都会转换成数值,再比较简单类型与引用类型比较,对象转化成其原始类型的值,再比较两个都为引用类型,则比较它们是否指向同一个对象null 和 undefined 相等存在 NaN 则返回 false二、全等操作符全等操作符由 3 个等于号( === )表示,只有两个操作数在不转换的前提下相等才返回 true。即类型相同,值也需相同let result1 = ("55" === 55); // false,不相等,因为数据类型不同
let result2 = (55 === 55); // true,相等,因为数据类型相同值也相同undefined 和 null 与自身严格相等let result1 = (null === null) //true
let result2 = (undefined === undefined) //true三、区别相等操作符(==)会做类型转换,再进行值的比较,全等运算符不会做类型转换let result1 = ("55" === 55); // false,不相等,因为数据类型不同
let result2 = (55 === 55); // true,相等,因为数据类型相同值也相同null 和 undefined 比较,相等操作符(==)为true,全等为falselet result1 = (null == undefined ); // true
let result2 = (null === undefined); // false小结相等运算符隐藏的类型转换,会带来一些违反直觉的结果'' == '0' // false
0 == '' // true
0 == '0' // true
false == 'false' // false
false == '0' // true
false == undefined // false
false == null // false
null == undefined // true
' \t\r\n' == 0 // true但在比较null的情况的时候,我们一般使用相等操作符==const obj = {};
if(obj.x == null){
console.log("1"); //执行
}等同于下面写法if(obj.x === null || obj.x === undefined) {
...
}使用相等操作符(==)的写法明显更加简洁了所以,除了在比较对象属性为null或者undefined的情况下,我们可以使用相等操作符(==),其他情况建议一律使用全等操作符(===)双等号 "==" 运算符用于比较两个值是否相等。在使用双等号进行比较时,JavaScript 会在必要时进行类型转换,然后再比较这两个值。这意味着,即使两个值的类型不同,双等号也会尝试将它们转换为相同的类型,然后再进行比较。例如:"1" == 1 这个表达式会返回 true,因为 JavaScript 将字符串 "1" 转换为数字 1 后再进行比较。三等号 "===" 运算符也用于比较两个值是否相等,但它不会进行类型转换。如果两个值的类型不同,直接返回 false。例如:"1" === 1 这个表达式会返回 false,因为一个是字符串,一个是数字,它们的类型不同。推荐在大多数情况下使用 "===" 运算符进行严格的值比较,因为它不会进行类型转换,可以避免一些潜在的错误。只有在明确知道两个值的类型,并且希望进行类型转换后再比较时,才应该使用 "==" 运算符。总之,了解 "==" 和 "===" 运算符的区别,以及在什么情况下使用它们,可以帮助开发者编写更可靠的 JavaScript 代码。
Ned
用Three.js搞个3D词云
2D词云经常用,是时候升级了,用一下3D词云!1.球坐标除了常见的笛卡尔坐标系,极坐标系,还有一种坐标系,球坐标。通过以坐标原点为参考点,由方位角、仰角和距离构成一个三维坐标。在three.js数学库里有个球坐标threejs.org/docs/#api/z…Spherical( radius : Float, phi : Float, theta : Float )radius:半径,或者说从该点到原点的(欧几里得距离,即直线距离)。默认值为1.0。范围[0,无穷)phi:与y轴(向上)的极角(以弧度为单位)。 默认值为 0。范围[0,PI]theta:绕y轴(向上)的赤道角(方位角)(以弧度为单位)。 默认值为 0。范围[0,2*PI]极角(phi)位于正 y 轴和负 y 轴上,与其的夹角。赤道角(方位角)(theta)从正 z 开始,环绕一圈。2.线性插值函数从开始值到结束值,映射到[0,1]的区间,通过[0,1]范围的值可以得到一个开始值到结束值之间的线性映射的值。 我们通常手动这样写原始值val范围min,max
新值value范围newMin,newMax
value=newMin+ ((val-min)/(max-min))*(newMax-newMin)在three.js数学工具里https://threejs.org/docs/?q=Math#api/zh/math/MathUtils.lerplerp(x:Float,y:Float,t:Float):Floatx:开始值。y:结束值。t:闭合区间[0,1]中的插值因子。返回基于给定间隔从两个已知点线性插值的值-t=0将返回x,t=1将返回y。使用value=lerp(newMin,newMax,(val-min)/(max-min))3.画文本canvas文本/**
*canvas文本
* @param {String} text 文本字符串
* @param {Number} fontSize 字体大小
* @param {String} color 颜色
* @returns
*/
export function getCanvasText(text, fontSize, color, bg) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = fontSize + 'px Arial';
ctx.fillStyle = color;
let padding = 5;
//测量文本大小,并设置canvas宽高预留padding
canvas.width = ctx.measureText(text + '').width + padding * 2;
canvas.height = fontSize * 1.2 + padding * 2;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = bg;
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.fill();
ctx.font = fontSize + 'px Arial';
ctx.fillStyle = color;
ctx.fillText(text, padding, fontSize + padding * 0.5);
return canvas;
}文本网格/***
* 文本网格
* @param {String} text 文本字符串
* @param {Number} fontSize 字体大小
* @param {String} color 颜色
*/
export function getTextMesh(THREE, text, fontSize, color) {
const canvas = getCanvasText(text, fontSize * 10, color, 'rgba(0,0,0,0)');
const map = new THREE.CanvasTexture(canvas);
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
//透明贴图
const canvasAlpha = getCanvasText(text, fontSize * 10, '#FFFFFF', 'rgba(0,0,0,0)');
const mapAlpha = new THREE.CanvasTexture(canvasAlpha);
map.wrapS = THREE.RepeatWrapping;
map.wrapT = THREE.RepeatWrapping;
const material = new THREE.MeshBasicMaterial({
map: map,
transparent: true,
side: THREE.DoubleSide
//设置透明贴图避免字体重叠
alphaTest: 0.5,
alphaMap: mapAlpha
});
const geometry = new THREE.PlaneGeometry(canvas.width * 0.1, canvas.height * 0.1);
const mesh = new THREE.Mesh(geometry, material);
return { material, geometry, canvas, mesh };
}注意:canvas贴图一定要放大倍数,否则会近看模糊, 为了保持大小,创建二维平面板时可以对应缩小比例canvas二维平面的贴图文本可能会出现深度冲突,导致文本之间遮挡,可以使用添加透明贴图和透明测试值,让文本背景颜色去掉,呈现正确的重叠顺序不推荐使用TextGeometry,因为要涵盖全部字体的typeface很大,且一旦文本多的时候,面数也多就很卡4.开搞计算文本坐标,将球面上的坐标点分成that.data.length个const vector = new THREE.Vector3();
const phi = Math.acos(THREE.MathUtils.lerp(-1, 1, idx / (that.data.length - 1)));
const theta = Math.sqrt(that.data.length * Math.PI) * phi;
vector.setFromSphericalCoords(that.radius, phi, theta);因为反余弦函数的值域范围刚好是[0,PI],定义域范围是[-1,1],那么我们可以通过lerp(-1,1,t)的线性插值函数得到对应的极角,这里使用的是数据索引idxsetFromSphericalCoords通过球坐标转成三维坐标//文本大小线性插值,根据数据值大小做映射
let s = THREE.MathUtils.lerp(
that.minFontSize,
that.maxFontSize,
(item.value - min) / size
);
let { mesh, geometry } = getTextMesh(THREE, text, s, that.color);
mesh.name = 'text' + idx;
mesh.position.set(vector.x, vector.y, vector.z);
textGroup.add(mesh);将文本坐标设置成球坐标转换后的笛卡尔三维坐标,我们会发现文本全部垂直,虽然在一个球体对应的坐标上,但是并没有形成一个友好的球体。我们需要的效果是让字体沿着球面摆放 geometry.lookAt(vector);
geometry.translate(vector.x, vector.y, vector.z);将二维平面几何看向坐标点,然后按着坐标点进行移动,即可得到一个文本球体可以增加个雾来增加层次感this.scene.fog = new THREE.FogExp2(new THREE.Color('#000000'), 0.003);3D词云在数据多的情况下很好看,但是数据少的时候就显得很空,这时候可以加个球框 const g = new THREE.IcosahedronGeometry(that.radius * 2, 2);
const m = new THREE.MeshBasicMaterial({
color: that.color,
transparent: true,
opacity: 0.2,
wireframe: true
});
const mm = new THREE.Mesh(g, m);
this.objGroup.add(mm);加个自动旋转 animateAction() {
if (this.objGroup) {
if (this.objGroup.rotation.y >= Math.PI * 2) {
this.objGroup.rotation.y = 0;
} else {
this.objGroup.rotation.y += 0.001;
}
}
}加个随机颜色 const color = `rgb(${Math.random() * 255},${Math.random() * 255},${
Math.random() * 255
})`;
let { mesh, geometry } = getTextMesh(THREE, text, s, color);GitHubhttps://github.com/xiaolidan00/my-three
Ned
《Three.js开发指南第2版》读书笔记1
内容来源于书籍【美】乔斯.德克森著,杨芬、赵汝达 译的《Three.js开发指南第2版》 以下为读书笔记,仅供学习《Three.js开发指南第2版》随书源码地址:github.com/josdirksen/…第1章 使用Three.js创建你的第一个三位场景<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="./libs/three.js"></script>
</head>
<body>
<div id="WebGL-output"> </div>
<script></script>
</body>
</html> //定义场景
var scene = new THREE.Scene();坐标轴 //创建轴并添加到场景中
var axies = new THREE.AxisHelper(20);
scene.add(axies); 添加平面 //定义平面 宽60,高20
var planeGeomentry = new THREE.PlaneGeometry(60, 20);
//材质
var planeMaterial = new THREE.MeshBasicMaterial({ color: 0xcccccc });
//合并网格对象
var plane = new THREE.Mesh(planeGeomentry, planeMaterial);
//绕x轴旋转90度
plane.rotation.x = -0.5 * Math.PI;
//场景中位置设置
plane.position.x = 15;
plane.position.y = 0;
plane.position.z = 0;
//将平面对象添加到场景中
scene.add(plane);摄像机
//定义摄像机
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
//设置摄像机的位置
camera.position.x = -30;
camera.position.y = 40;
camera.position.z = 30;
//摄像机指向场景中心默认(0,0,0)
camera.lookAt(scene.position);渲染器 //定义渲染器
var renderer = new THREE.WebGLRenderer();
renderer.setClearColorHex();
//设置场景的背景颜色
renderer.setClearColor(new THREE.Color(0xeeeeee));
//设置场景的大小
renderer.setSize(window.innerWidth, window.innerHeight);
//输出元素到对应HTML元素中
document.getElementById('WebGL-output').appendChild(renderer.domElement);
//渲染器使用指定的摄像机渲染场景
renderer.render(scene, camera);添加物体对象 //长方体
var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
var cubeMaterial = new THREE.MeshBasicMaterial({color: 0xff0000, wireframe: true});//材质框架wireframe
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.x = -4;
cube.position.y = 3;
cube.position.z = 0;
scene.add(cube);
//球体
var sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
var sphereMaterial = new THREE.MeshBasicMaterial({color: 0x7777ff, wireframe: true});
var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.x = 20;
sphere.position.y = 4;
scene.add(sphere);点光源 //定义点光源
var spotLight = new THREE.SpotLight(0xffffff);
//点光源位置
spotLight.position.set(-40, 60, -10);
//点光源产生投影
spotLight.castShadow = true;
scene.add(spotLight);材质材质MeshBasicMaterial不会对光源有任何反应,指挥使用指定的眼色渲染物体材质MeshLambertMaterial MeshPhongMaterial在渲染时会对光源产生反应//没有设置光源会对象会全黑
var cubeMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 });投影//渲染器开启渲染阴影效果
renderer.shadowMapEnabled = true;
//平面接收投影
plane.receiveShadow = true;
//点光源产生投影
spotLight.castShadow = true;
//物体对象产生投影
cube.castShadow = true;动画 function renderScene() {
//保持动画能够持续运行
requestAnimationFrame(renderScene);
renderer.render(scene, camera);
}帧数统计图形 <script src="./libs/stats.js"></script>
<div id="Stats-output"></div>function initStats() {
var stats = new Stats();
stats.setMode(0);
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById('Stats-output').appendChild(stats.domElement);
return stats;
}
var stats = initStats();
function renderScene() {
//保持动画能够持续运行
//帧数统计图更新
stats.update();
requestAnimationFrame(renderScene);
renderer.render(scene, camera);
}
renderScene();
立方体旋转 function renderScene() {
//保持动画能够持续运行
stats.update();
//旋转立方体
cube.rotation.x += 0.02;
cube.rotation.y += 0.02;
cube.rotation.z += 0.02;
requestAnimationFrame(renderScene);
renderer.render(scene, camera);
}球体弹跳 var step = 0;
function renderScene() {
//保持动画能够持续运行
stats.update();
//球体弹跳
step += 0.04;
sphere.position.x = 20 + 10 * Math.cos(step);
sphere.position.y = 2 + 10 * Math.abs(Math.sin(step));
requestAnimationFrame(renderScene);
renderer.render(scene, camera);
}datGUI控制小球弹跳和立方体的旋转的速度<script src="./libs/dat.gui.js"></script>//js对象
var controls = new (function () {
this.rotationSpeed = 0.02;
this.bouncingSpeed = 0.03;
})();
var gui = new dat.GUI();
//速度范围在0~0.5
gui.add(controls, 'rotationSpeed', 0, 0.5);
gui.add(controls, 'bouncingSpeed', 0, 0.5);
function renderScene() {
//保持动画能够持续运行
stats.update();
//旋转立方体
let speed = controls.rotationSpeed;
cube.rotation.x += speed;
cube.rotation.y += speed;
cube.rotation.z += speed;
step += controls.bouncingSpeed;
sphere.position.x = 20 + 10 * Math.cos(step);
sphere.position.y = 2 + 10 * Math.abs(Math.sin(step));
requestAnimationFrame(renderScene);
renderer.render(scene, camera);
}场景自适应浏览器 function onResize() {
//屏幕长宽比调整
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
//渲染器尺寸调整
renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', onResize, false);第2章 构建Three.js场景的基本组件环境光 var ambientLight = new THREE.AmbientLight(0x0c0c0c);
scene.add(ambientLight);datGUI控制对象添加var controls = new function () {
this.rotationSpeed = 0.02;
//场景中的对象个数
this.numberOfObjects = scene.children.length;
//移除最新添加的立方体
this.removeCube = function () {
var allChildren = scene.children;
var lastObject = allChildren[allChildren.length - 1];
if (lastObject instanceof THREE.Mesh) {
scene.remove(lastObject);
this.numberOfObjects = scene.children.length;
}
};
//添加随机立方体
this.addCube = function () {
var cubeSize = Math.ceil((Math.random() * 3));
var cubeGeometry = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
var cubeMaterial = new THREE.MeshLambertMaterial({color: Math.random() * 0xffffff});
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.castShadow = true;
cube.name = "cube-" + scene.children.length;
//立方体随机位置
cube.position.x = -30 + Math.round((Math.random() * planeGeometry.parameters.width));
cube.position.y = Math.round((Math.random() * 5));
cube.position.z = -20 + Math.round((Math.random() * planeGeometry.parameters.height));
scene.add(cube);
this.numberOfObjects = scene.children.length;
};
//输出对象信息
this.outputObjects = function () {
console.log(scene.children);
}
}; var gui = new dat.GUI();
gui.add(controls, 'rotationSpeed', 0, 0.5);
gui.add(controls, 'addCube');
gui.add(controls, 'removeCube');
gui.add(controls, 'outputObjects');
gui.add(controls, 'numberOfObjects').listen();
function render() {
stats.update();
//立方体旋转
scene.traverse(function (e) {
if (e instanceof THREE.Mesh && e != plane) {
e.rotation.x += controls.rotationSpeed;
e.rotation.y += controls.rotationSpeed;
e.rotation.z += controls.rotationSpeed;
}
});
// render using requestAnimationFrame
requestAnimationFrame(render);
renderer.render(scene, camera);
}THREE.Scene.Add:向场景中添加对象THREE.Scene.Remove:移除场景中的对象THREE.Scene.children:获取场景中的所有的子对象列表THREE.Scene.getObjectByName:利用name属性,获取场景中特定的对象给场景添加雾化效果//0xffffff白色雾化效果 0.015近处的属性值 100远处的属性值 雾的浓度线性增长
scene.fog = new THREE.Fog(0xffffff, 0.015, 100);
//另一种雾化效果,浓度0.01,浓度随距离指数增长
scene.fog=new THREE.FogExp2( 0xffffff, 0.01 );使用overrideMaterial属性场景中的所有物体都会使用该属性的材质,即便自身设置了材质scene.overrideMaterial = new THREE.MeshLambertMaterial({color: 0xffffff});MeshLambertMaterial创建出不发光但可以对场景中的光源产生光源的物体THREE.Scene常用的方法和属性方法、属性描述add(object)向场景中添加对象,可创建对象组children返回场景中所有对象的数组,包括摄像机和光源getObjectByName(name,recursive)创建对象时可指定唯一标识的name,使用该刚发可以查找特定名字的对象.recursive=false在调用者子元素上查找,recursive=true在调用者的所有后台对象上查找remove(object)object场景中对象的引用,将对象从场景中移除traverse(function)children属性可返回场景中的所有物体,用于遍历调用者和调用者所有的后代,被调用者和每一个后代对象调用function方法fog为场景添加雾化效果,可产生隐藏远处物体的雾化效果overrideMaterial强制场景中的所有物体使用相同的材质几何体//构成几何体的顶点
var vertices = [
new THREE.Vector3(1, 3, 1),
new THREE.Vector3(1, 3, -1),
new THREE.Vector3(1, -1, 1),
new THREE.Vector3(1, -1, -1),
new THREE.Vector3(-1, 3, -1),
new THREE.Vector3(-1, 3, 1),
new THREE.Vector3(-1, -1, -1),
new THREE.Vector3(-1, -1, 1)
];
//保存由顶点链接起来创建的三角形面
var faces = [
//new THREE.Face3(0, 2, 1)使用vertices数组中的点0,2,1创建而成的三角面
new THREE.Face3(0, 2, 1),
new THREE.Face3(2, 3, 1),
new THREE.Face3(4, 6, 5),
new THREE.Face3(6, 7, 5),
new THREE.Face3(4, 5, 1),
new THREE.Face3(5, 0, 1),
new THREE.Face3(7, 6, 2),
new THREE.Face3(6, 3, 2),
new THREE.Face3(5, 7, 0),
new THREE.Face3(7, 2, 0),
new THREE.Face3(1, 3, 4),
new THREE.Face3(3, 6, 4),
];
//实例化几何对象
var geom = new THREE.Geometry();
geom.vertices = vertices;
geom.faces = faces;
//three.js会决定每个面的法向量,法向量用于决定不同光源下的颜色
geom.computeFaceNormals();注意创建面的顶点时创建顺序,顶点顺序决定了某个面是面向摄像机还是背向摄像机的创建面向摄像机的面,顶点的顺序是顺时针的,反之逆时针对于渲染器和游戏引擎来说,使用三角形更加容易,三角形渲染起来效率更高//render动画循环中
function render(){
//....
mesh.children.forEach(function (e) {
//几何体网格指向一个更新后的顶点数组
e.geometry.vertices = vertices;
//告诉几何对象顶点更新
e.geometry.verticesNeedUpdate = true;
//重新计算每个面
e.geometry.computeFaceNormals();
});
//...
}多种材质创建网格var materials = [
new THREE.MeshLambertMaterial({opacity: 0.6, color: 0x44ff44, transparent: true}),
new THREE.MeshBasicMaterial({color: 0x000000, wireframe: true})
];
var mesh = THREE.SceneUtils.createMultiMaterialObject(geom, materials);
//子对象都添加阴影
mesh.children.forEach(function (e) {
e.castShadow = true
});复制一个对象this.clone = function () {
var clonedGeometry = mesh.children[0].geometry.clone();
var materials = [
new THREE.MeshLambertMaterial({opacity: 0.6, color: 0xff44ff, transparent: true}),
new THREE.MeshBasicMaterial({color: 0x000000, wireframe: true})
];
//创建要复制的对象的新网格
var mesh2 = THREE.SceneUtils.createMultiMaterialObject(clonedGeometry, materials);
mesh2.children.forEach(function (e) {
e.castShadow = true
});
//移动新创建的网格,删除之前的副本(若存在)并把这个副本添加到场景中
mesh2.translateX(5);
mesh2.translateZ(5);
mesh2.name = "clone";
scene.remove(scene.getChildByName("clone"));
scene.add(mesh2);
}THREE.SceneUtils.createMultiMaterialObject为几何体添加线框THREE.WireframeHelper()也可以添加线框//为网格mesh添加线框,线框颜色0x000000
var helper=new THREE.WireframeHelper(mesh,0x000000);
scene.add(helper)helper实际上THREE.Line对象,可设置线框如何显示,如helper.material.linewidth=2指定线框的宽度网格对象的属性和方法方法、属性描述position对象相对于父对象的位置,通常父对象为THREE.Scene对象或THREE.Object3D对象rotation设置绕每个轴旋转的旋转弧度,ThreeJs还提供了设置相对特定轴的旋转弧度的方法:rotateX(),rotateY(),rotateZ()scale沿x,y,z轴缩放对象translateX(amount)沿x轴将对象平移amount距离translateY(amount)沿y轴将对象平移amount距离translateZ(amount)沿z轴将对象平移amount距离visible该属性为false时,Mesh将不会被渲染到场景中设置position位置cube.position.x=10
cube.position.y=3
cube.position.z=1
//一次性设置xyz坐标值
cube.position.set(10,3,1)
cube.positon=new THREE.Vector3(10,3,1)THREE.SceneUtils.createMultiMaterialObject创建一个多材质对象,返回的是网格组改变网格组中某个对象的位置,可看到两个不同的THREE.Mesh对象移动网格组,它们的偏移量是一样的设置rotation旋转cube.rotation.x=0.5*Math.PI
cube.rotation.set(0.5*Math.PI,0,0)
cube.rotation=new THREE.Vector3(0.5*Math.PI,0,0)使用度数(0~360)var degree=45
var inRadians=degree*(Math.PI/180)scale缩放值大于1放大,反之缩小cube.scale.x=1.5
cube.scale.set(1.5,1,1)
cube.scale=new THREE.Vector3(1.5,1,1)移动相对于当前位置的平移距离cube.translateX(10);
cube.translateY(-10);
cube.translateZ(5);
摄像机透视投影摄像机,对象距离摄像机越远会被渲染得越小正交投影摄像机,所有立方体被渲染出来的尺寸都一样,对象相对于摄像机的距离不影响渲染结果 var controls = new function () {
this.perspective = "Perspective";
//切换摄像机
this.switchCamera = function () {
if (camera instanceof THREE.PerspectiveCamera) {
//正交投影摄像机
camera = new THREE.OrthographicCamera(window.innerWidth / -16, window.innerWidth / 16, window.innerHeight / 16, window.innerHeight / -16, -200, 500);
camera.position.x = 120;
camera.position.y = 60;
camera.position.z = 180;
camera.lookAt(scene.position);
this.perspective = "Orthographic";
} else {
//透视摄像机
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.x = 120;
camera.position.y = 60;
camera.position.z = 180;
camera.lookAt(scene.position);
this.perspective = "Perspective";
}
};
};透视摄像机new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000)参数描述fovfov表示视场,摄像机中能看到的那部分场景,推荐默认值50aspect(长宽比)渲染结果的横向尺寸和纵向尺寸的比值,推荐默认值window.innerWidth / window.innerHeightnear(近面距离)从距离摄像机多近的距离开始渲染,通常设置尽量小,从而能够渲染从摄像机位置可看到的所有物体,推荐默认值0.1far(远面距离)摄像机从它所在的位置能够看到多远,推荐默认值:1000zoom(变焦)可放缩场景,负数时场景上下颠倒,推荐默认值:1正交投影摄像机 new THREE.OrthographicCamera(window.innerWidth / -16, window.innerWidth / 16, window.innerHeight / 16, window.innerHeight / -16, -200, 500);参数描述left(左边界)可视范围的做屏幕,渲染不放的左边界,负值将不会看到,超过边界不会被看得right(右边界)可渲染区域的另一个侧面top(上边界)可渲染区域的最上面bottom(下边界)可渲染区域的最下面near(近面距离)基于摄像机所处位置,从这一点开始渲染场景far(远面距离)基于摄像机所处位置,渲染场景到这一点位置zoom(变焦)放缩场景将摄像机聚焦在指定点上camera.lookAt(new THREE.Vector3(x,y,z))lookAt可以追随物体,看向特定的网格camera.lookAt(mesh.position)
Ned
一站式掌握JavaScript操作元素与DOM节点的完全指南
获取元素<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="box">
<p class="hehe">呵呵</p>
<input type="text" name="txt" id="">
<h3>三级标题</h3>
<input type="text" name="txt" id="">
<p class="hehe">呵呵</p>
</div>
<p class="hehe">呵呵</p>
<input type="text" name="" id="">
<p>呵呵</p>
<script>
//1. 获取页面中所有的p标题
var body_p = document.getElementsByTagName('p');
console.log(body_p);
//2. 获取所有类名是hehe的标签
// var p_hehe = document.getElementsByClassName('hehe');
// console.log(p_hehe);
//3. 获取所有的name是txt的表单
var input_name = document.getElementsByName('txt');
console.log(input_name);
//4. 获取div#box里面的class=hehe的标签
// var box_hehe = document.getElementById('box').getElementsByClassName('hehe');
// console.log(box_hehe);
//兼容IE9以下的byClassName
//class是保留字
function byClassName(obj,className){
//判断是否兼容
if(obj.getElementsByClassName){ //支持这个方法,为true
return obj.getElementsByClassName(className);
}else{ //不支持这个方法,false
//声明一个空数组
var arr = [];
//获取所有元素
var eles = obj.getElementsByTagName('*');
//遍历所有元素
for(var i = 0,len = eles.length;i < len;i ++){
//判断每一个元素是否有指定的类名
if(eles[i].className === className){
arr.push(eles[i]);
}
}
return arr;
}
}
var box_hehe = byClassName(document.getElementById('box'),'hehe');
console.log(box_hehe);
let query_hehe = document.querySelectorAll('.hehe');
</script>
</body>
</html>操作元素样式ele.style访问或设置行内样式2. window.getComputedStyle()//获取非行内样式
//标准浏览器
// alert(getComputedStyle(o_div,1).width);
//IE浏览器
// alert(o_div.currentStyle.width);
//兼容
function getStyle(obj,attr){
return obj.currentStyle ? obj.currentStyle[attr] : getComputedStyle(obj,true)[attr];
// return window.getComputedStyle ? getComputedStyle(obj,1)[attr] : obj.currentStyle[attr];
} <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- <style>
#box{
width:100px;
height: 100px;
background: red;
}
</style> -->
</head>
<body>
<div id="box"></div>
<script>
//1. 获取元素
var o_box = document.querySelector('#box');
//2. 添加样式
o_box.style.width = '100px';
o_box.style.height = '100px';
o_box.style.background = 'red';
// o_box.style.cssText = 'width:100px;height:100px;background:red';
</script>
</body>
</html>div宽度变小<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#box{
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<div id="box"></div>
<script>
//1. 获取元素
var o_box = document.querySelector('#box');
//2. 获取宽度
// var width = parseInt(o_box.style.width);
// alert(width);
//标准浏览器获取非行内样式的方法
// var width = getComputedStyle(o_box).width;
//IE浏览器获取非行内样式的方法
// var width = o_box.currentStyle.width;
//兼容
//obj : 指定的标签对象
//attribute : 属性 attr 样式属性
function getStyle(obj,attr){
return obj.currentStyle ? obj.currentStyle[attr] : getComputedStyle(obj)[attr];
}
// var width = getStyle(o_box,'width');
// alert(width);
//3. 逐渐变小
setInterval(function(){
o_box.style.height = parseInt(getStyle(o_box,'height')) - 1 + 'px';
},30)
</script>
</body>
</html>操作元素类名ele.className<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.active{
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<input type="button" value="on|off">
<div id="box"></div>
<script>
//1. 获取元素
var div = document.querySelector('#box');
var onoff = document.querySelector('input');
//2. 添加类名
div.className = 'pox';
// div.className = 'hehe';
div.classList.add('hehe'); //添加新类名
div.classList.add('haha');
div.classList.add('active');
//获取所有类名
// console.log(div.classList,div.classList.length);
//通过下标获取指定的类名
// console.log(div.classList.item(2));
//3. 添加事件
onoff.onclick = function(){
div.classList.toggle('active');
}
//4. 删除指定类名
div.classList.remove('hehe');
//5. 查看指定的类名是否存在
console.log(div.classList.contains('hehe'));
</script>
</body>
</html>ele.classListele.classList : 获取元素的全部类名ele.classList.lentgh: 获取到元素类名的数量ele.classList.add(): 向元素添加一个或多个类名ele.classList.remove() : 可以删除元素的一个或多个类名ele.classList.item(index) : 可以获取到元素类名索引为index的类名ele.classList.toggle() : 可以为元素切换类名ele.classList.contains(x) : 查看元素是否存在类名为"x"的类操作元素属性原生属性操作元素.属性 元素['属性'] 元素.getAttribute('属性名') 元素.setAttribute('属性名','属性值') 元素.removeAttribute('属性名')<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="box" title="标题" data-id="sp1"></div>
<script>
//1. 获取元素
var div = document.getElementById('box');
//2. 获取属性
// console.log(div.id,div['title']);
// console.log(div.name,div.hehe);
//3. 获取自定义属性
// console.log(div.getAttribute('name'));
// //4. 设置自定义属性
// div.setAttribute('heihei','嘿嘿');
// //5. 删除自定义属性
// div.removeAttribute('name');
div.dataset.cartId = 999;
console.log(div.dataset.id);
div.id = '';
</script>
</body>
</html>自定义属性操作getAttributesetAttributeremoveAttribute元素.getAttribute('属性名') : 获取属性 元素.setAttribute('属性名','属性值') : 设置属性 元素.removeAttribute('属性名') : 删除属性H5自定义属性的操作 ( data-* )ele.dataset : 读写自定义属性<body>
<div id="box" data-my-id="me"></div>
<script>
var o_div = document.getElementById('box');
console.log(o_div.dataset.myId); //'me'
</script>
</body>操作元素内容innerHTML : 设置或获取当前节点内容的内容(超文本)(可以解析超文本的含义)innerText : 设置或获取当前节点内部的内容(纯文本)(不能解析超文本)value : 设置或获取表单中的内容<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!-- <input type="text" name="" id="" value="请输入姓名:">
<div id="box">呵呵<strong>哈哈</strong>嘿嘿</div> -->
<input type="text" name="" id="">
<div id="box"></div>
<script>
//1. 获取元素
var txt = document.querySelector('input');
var div = document.querySelector('#box');
//3. 添加内容
txt.value = '姓名:';
// div.innerText = '呵呵<i>嘿嘿</i>嘻嘻';
div.innerHTML = '呵呵<i>嘿嘿</i>嘻嘻';
//2. 获取元素中的内容
console.log(txt.value);
console.log(div.innerHTML);
console.log(div.innerText);
</script>
</body>
</html>获取元素尺寸(只能获取,不能设置)offsetWidth / offsetHeight节点对象.offsetWidth : 获取当前节点对象的相对宽度(width + border + padding)节点对象.offsetHeight : 获取当前节点对象的相对高度(height + border + padding)clientWidth / clientHeight节点对象.clientWidth : 获取当前节点对象的可视区宽度(width + padding)节点对象.clientHeight : 获取当前节点对象的可视区高度(height + padding)<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#box{
width: 100px;
height: 100px;
background: red;
border: 1px solid black;
padding: 5px;
}
</style>
</head>
<body>
<div id="box"></div>
<script>
//1. 获取元素
var div = document.querySelector('#box');
//2. 获取div的相对宽高
//width + padding + border
//height + padding + border
console.log(div.offsetWidth,div.offsetHeight); //112 112
//width/height + padding
console.log(div.clientWidth,div.clientHeight); //110 110
</script>
</body>
</html>获取元素偏移量offsetLeft / offsetTopoffsetLeft : 相对左边的距离如果父元素没有定位,则当前元素相对于页面左边(body)的left值如果父元素有定位,则当前元素相对于父元素左边的left值。offsetTop : 相对上边的距离如果父元素没有定位,则当前元素相对于页面上边(body)的top值如果父元素有定位,则当前元素相对于父元素上边的top值。clientLeft / clientTopclientLeft : 表示一个元素的左边框的宽度,以像素表示。clientTop : 表示一个元素的上边框的宽度,以像素表示。<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#box{
width: 300px;
height: 300px;
background: red;
}
.pox{
width: 100px;
height: 100px;
background: green;
position: absolute;
left: 100px;
top: 100px;
border: 10px solid purple;
}
</style>
</head>
<body>
<!-- box的父亲是body -->
<div id="box">
<!-- pox的父亲是box -->
<div class="pox"></div>
</div>
<script>
// 1. 如果没有定位,则相对于页面(body)边的距离
// 2. 如果有定位,则相对于父元素边距离。
//1. 获取元素
var div = document.querySelector('#box');
var pox = document.querySelector('.pox');
//2. 添加事件
// 1. 如果没有定位,则相对于页面(body)边的距离
console.log(div.offsetLeft,div.offsetTop); // 8 8
// 2. 如果有定位,则相对于父元素边距离。
console.log(pox.offsetLeft,pox.offsetTop); //100 100
console.log(pox.clientLeft); //边框的宽度
</script>
</body>
</html>DOM节点及以节点操作DOM节点DOM 的节点我们一般分为常用的三大类 元素节点 / 文本节点 / 属性节点什么是分类,比如我们在获取元素的时候,通过各种方法获取到的我们叫做元素节点(标签节点)比如我们标签里面写的文字,那么就是文本节点写在每一个标签上的属性,就是属性节点获取节点firstChild : 第一个子节点firstElementChild : 第一个元素子节点lastChild : 最后一个子节点lastElementChild : 最后一个元素子节点previousSibling : 前一个兄弟节点previousElementSibling : 前一个元素兄弟节点nextSibling : 下一个兄弟节点nextElementSibling : 下一个元素兄弟节点parentNode : 父节点childNodes : 获取到所有的元素子节点 与 文本子节点//删除空白文本子节点
function noSpaceNode(node) {
//获取所有子节点
var childs = node.childNodes;
//遍历所有的子节点
for (var i = 0; i < childs.length; i++) {
//文本 空白
if (childs[i].nodeType === 3 && /^\s+$/.test(childs[i].nodeValue)) {
//删除空白节点
node.removeChild(childs[i]);
}
}
return node;
}11. children : 获取所有的元素子节点节点属性nodeTypenodeType:获取节点的节点类型,用数字表示nodeNamenodeName:获取节点的节点名称nodeValuenodeValue: 获取节点的值汇总-nodeTypenodeNamenodeValue元素节点1大写标签名null属性节点2属性名属性值文本节点3#text文本内容创建节点document.createElement('标签名') : 创建元素节点document.createTextNode('文本') : 创建文本节点document.createDocumentFragment() : 创建文档碎片 (为了减少与页面的交互,提高性能)<body>
<ul></ul>
<script>
//1. 获取页面元素
var ul = document.querySelector('ul');
//2. 假设我们从后端获取到一组数据
var arr = ['呵呵','哈哈','嘿嘿','嘻嘻','咕咕','嘎嘎'];
//链接的数组
var link = ['http://www.baidu.com','http://www.taobao.com','http://jd.com','http://1000phone.com','http://www.qq.com','http://www.sina.com.cn'];
//3. 遍历数组
arr.forEach(function(item,index){
//1. 创建一个li
var li = document.createElement('li');
//2. 创建a
var a = document.createElement('a');
//3. 设置元素属性
a.href = link[index];
//4. 设置元素内容
// a.innerText = item;
// 创建一个文本节点
var txt = document.createTextNode(item);
// 文本节点添加到a元素中
a.appendChild(txt);
//5. 将a元素添加到li中
li.appendChild(a);
//6. 将li添加到ul中
ul.appendChild(li);
})
</script>
</body>插入节点父节点.appendChild(子节点) : 将这个子节点追加到父节点中子节点列表的末尾。父节点.insertBefore(新节点,指定的旧的节点) : 在指定的旧节点前面插入新节点<body>
<ul></ul>
<script>
//1. 获取页面元素
var ul = document.querySelector('ul');
//创建一个文档碎片
var fragment = document.createDocumentFragment();
//2. 添加10个元素
for(var i = 0;i < 10;i ++){
//创建li
var li = document.createElement('li');
//li中添加内容
li.innerText = i + 1;
//li添加到ul中
// ul.appendChild(li);
fragment.appendChild(li);
}
ul.appendChild(fragment);
//创建一个li
var li = document.createElement('li');
//文本节点
var txt = document.createTextNode('long long a go');
//txt 添加到li中
li.appendChild(txt);
//li 插入第一个子节点的前面
ul.insertBefore(li,ul.children[0]);
</script>
</body>修改节点父节点.replaceChild(新节点,旧节点) : 替换节点<body>
<ul></ul>
<script>
//1. 获取页面元素
var ul = document.querySelector('ul');
//创建一个文档碎片
var fragment = document.createDocumentFragment();
//2. 添加10个元素
for(var i = 0;i < 10;i ++){
//创建li
var li = document.createElement('li');
//li中添加内容
li.innerText = i + 1;
//li添加到ul中
// ul.appendChild(li);
fragment.appendChild(li);
}
ul.appendChild(fragment);
//创建一个li
var li = document.createElement('li');
//文本节点
var txt = document.createTextNode('long long a go');
//txt 添加到li中
li.appendChild(txt);
//li 插入第一个子节点的前面
ul.insertBefore(li,ul.children[0]);
//创建一个新的文本节点
var new_txt = document.createTextNode('good good study,day day up');
//替换
li.replaceChild(new_txt,txt);
</script>
</body>删除节点父节点.removeChild(子节点) : 删除子节点当前节点.remove() : 删除当前节点<body>
<p id="parent">呵呵</p>
<script>
//1. 获取页面元素
var p = document.querySelector('p');
//2. 删除子节点
p.removeChild(p.firstChild);
//3. 删除自己
p.remove();
</script>
</body>克隆节点当前节点.cloneNode([true]) : 复制节点<body>
<p id="parent">呵呵</p>
<script>
//1. 获取页面元素
var p = document.querySelector('p');
// 获取body
//获取head document.head
//获取html document.documentElement
var body = document.body;
//2. 添加事件
p.onclick = function(){
body.appendChild(this.cloneNode(true));
}
</script>
</body>false(默认) : 只克隆当前节点,不包含内容。 true : 克隆当前节点,包含内容
Ned
JavaScript中的发布订阅和观察者模式:如何优雅地处理事件和数据更新
一、观察者模式观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯例如生活中,我们可以用报纸期刊的订阅来形象的说明,当你订阅了一份报纸,每天都会有一份最新的报纸送到你手上,有多少人订阅报纸,报社就会发多少份报纸报社和订报纸的客户就形成了一对多的依赖关系实现代码如下:被观察者模式class Subject {
constructor() {
this.observerList = [];
}
addObserver(observer) {
this.observerList.push(observer);
}
removeObserver(observer) {
const index = this.observerList.findIndex(o => o.name === observer.name);
this.observerList.splice(index, 1);
}
notifyObservers(message) {
const observers = this.observeList;
observers.forEach(observer => observer.notified(message));
}
}观察者:class Observer {
constructor(name, subject) {
this.name = name;
if (subject) {
subject.addObserver(this);
}
}
notified(message) {
console.log(this.name, 'got message', message);
}
}使用代码如下:const subject = new Subject();
const observerA = new Observer('observerA', subject);
const observerB = new Observer('observerB');
subject.addObserver(observerB);
subject.notifyObservers('Hello from subject');
subject.removeObserver(observerA);
subject.notifyObservers('Hello again');上述代码中,观察者主动申请加入被观察者的列表,被观察者主动将观察者加入列表二、发布订阅模式发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在实现代码如下:class PubSub {
constructor() {
this.messages = {};
this.listeners = {};
}
// 添加发布者
publish(type, content) {
const existContent = this.messages[type];
if (!existContent) {
this.messages[type] = [];
}
this.messages[type].push(content);
}
// 添加订阅者
subscribe(type, cb) {
const existListener = this.listeners[type];
if (!existListener) {
this.listeners[type] = [];
}
this.listeners[type].push(cb);
}
// 通知
notify(type) {
const messages = this.messages[type];
const subscribers = this.listeners[type] || [];
subscribers.forEach((cb, index) => cb(messages[index]));
}
}发布者代码如下:class Publisher {
constructor(name, context) {
this.name = name;
this.context = context;
}
publish(type, content) {
this.context.publish(type, content);
}
}订阅者代码如下:class Subscriber {
constructor(name, context) {
this.name = name;
this.context = context;
}
subscribe(type, cb) {
this.context.subscribe(type, cb);
}
}使用代码如下:const TYPE_A = 'music';
const TYPE_B = 'movie';
const TYPE_C = 'novel';
const pubsub = new PubSub();
const publisherA = new Publisher('publisherA', pubsub);
publisherA.publish(TYPE_A, 'we are young');
publisherA.publish(TYPE_B, 'the silicon valley');
const publisherB = new Publisher('publisherB', pubsub);
publisherB.publish(TYPE_A, 'stronger');
const publisherC = new Publisher('publisherC', pubsub);
publisherC.publish(TYPE_C, 'a brief history of time');
const subscriberA = new Subscriber('subscriberA', pubsub);
subscriberA.subscribe(TYPE_A, res => {
console.log('subscriberA received', res)
});
const subscriberB = new Subscriber('subscriberB', pubsub);
subscriberB.subscribe(TYPE_C, res => {
console.log('subscriberB received', res)
});
const subscriberC = new Subscriber('subscriberC', pubsub);
subscriberC.subscribe(TYPE_B, res => {
console.log('subscriberC received', res)
});
pubsub.notify(TYPE_A);
pubsub.notify(TYPE_B);
pubsub.notify(TYPE_C);上述代码,发布者和订阅者需要通过发布订阅中心进行关联,发布者的发布动作和订阅者的订阅动作相互独立,无需关注对方,消息派发由发布订阅中心负责三、区别两种设计模式思路是一样的,举个生活例子:观察者模式:某公司给自己员工发月饼发粽子,是由公司的行政部门发送的,这件事不适合交给第三方,原因是“公司”和“员工”是一个整体发布-订阅模式:某公司要给其他人发各种快递,因为“公司”和“其他人”是独立的,其唯一的桥梁是“快递”,所以这件事适合交给第三方快递公司解决上述过程中,如果公司自己去管理快递的配送,那公司就会变成一个快递公司,业务繁杂难以管理,影响公司自身的主营业务,因此使用何种模式需要考虑什么情况两者是需要耦合的两者区别如下图:在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)设计模式设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。单例模式单个实例,只有一个对象,多次创建,返回同一个对象。单例模式的核心:确保只有一个实例,并提供全局访问发布订阅模式观察者模式又叫发布-订阅模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。案例流感期间,小明去药店买口罩,然而到店之后却被店员告知口罩已经售罄。这时,小明给店员留了一个电话,告诉店员等口罩到货后打电话通知他一下, 这样小明就可以在口罩到货后的第一时间去药店买口罩了。小明买口罩的过程就是对发布-订阅模式的一次实践:小明将电话留给店员,让店员在口罩到货后通知他,便是一次“订阅”;店员在口罩到货后,打电话告诉小明,便是一次“发布”。不过,在上面的例子中发布-订阅模式并不是唯一的解决方案,其优势也并没有体现出来。比如,小明不想把自己的电话留给药店,而是把药店的电话记了下来,每天给药店打电话去问。从目前的情况看,这种方式也是可以工作的。但往往我们需要面临更复杂的情况:当前疫情严重,不止小明一人需要口罩,小红、小白、小黑等许许多多的人也都需要口罩,他们也都想在口罩到货的第一时间得到消息,于是他们每个人每天都需要给药店打电话询问。这样药店每天都会收到几百个电话,店员们每天都被一个简单却重复的问题搞得很疲惫,而小明也每天都担心电话打晚了会被别人先得到消息“抢”走口罩。当有许多人都想获得“口罩到货”这一相同消息时,发布-订阅模式的优势就显示出来了。只要所有想知道这个消息的用户都在药店留一个电话,当口罩到货后,店员再给“订阅”这一事件的所有用户都打电话告知一下就可以了。这样用户就不用每天都给药店打电话询问,药店也不需要每天都花大量的时间接听电话了。//发布者-药店
let drugstore = {
//可在订阅事件的客户列表
clientList: {},
//添加事件订阅对象的方法
listen(eventName, fn) {
if (!this.clientList[eventName]) {
//如果客户订阅的事件当前不存在,则初始化这个事件
this.clientList[eventName] = [];
}
//如果存在,将客户端的联系方法添加到数组中
this.clientList[eventName].push(fn);
},
//向订阅对象发布消息的方法
publish(eventName, data) {
this.clientList[eventName].map(fn => {
//将需要发布的数据作为参数,调用客户在订阅时传入的回调函数
fn(data);
})
}
}
//订阅者-用户
//口罩到货后,小明要做的事件
function xiaoMing(data) {
console.log('口罩到货了,我要赶紧去买一些!');
}
//小明监听口罩到货事件
drugstore.listen('haveMask', xiaoMing);
// 口罩到货后,小红要做的事情
function xiaoHong(data) {
if (new Date() > new Date('2022/8/31')) {
console.log('疫情应该结束了,不买口罩了')
} else {
console.log('赶紧去买口罩,不然就买不着了')
}
}
// 小红监听口罩到货事件
drugstore.listen('haveMask', xiaoHong);
// 口罩到货后,小白要做的事情
function xiaoBai(data) {
if (data.price > 100) {
console.log('这口罩咋这么贵?不买了!')
} else if (data.price > 50) {
console.log('这口罩偏贵,先买10个用着,过段时间看能不能降价')
} else {
console.log('这批口罩价格还可以,买50个屯着')
}
}
// 小白监听口罩到货事件
drugstore.listen('haveMask', xiaoBai)
// 假设到货口罩的相关信息如下
const data = {
type: 'N95',
price: 30,
num: 1000
}
// 发布口罩到货的消息,并把口罩的相关信息作为参数传递给订阅该事件回调函数
// 收到信息后具体怎么处理,完全由回调函数自己定义,“药店”并不关心
drugstore.publish('haveMask', data)
Ned
巧妙驾驭JavaScript字符串:常用方法大揭秘
数组去重案例function fnNoRepeatOfArray(arr){
var list = [];
arr.forEach(function(value){
if(list.indexOf(value) === -1){
list.push(value);
}
})
return list;
}
function fn(arr){
for(var i = 0,len = arr.length;i < len;i ++){
for(var j = i + 1;j < len;j ++){
if(arr[i] === arr[j]){
arr.splice(j,1);
}
}
}
return arr;
}
function fn(arr){
var list = [];
arr.sort(function(a,b){return a - b;});
arr.forEach(function(value,index){
if(value !== arr[index + 1]){
list.push(value);
}
})
return list;
}字符串常用方法如何声明字面量方式: '' ""构造函数方式: new String() <script>
//创建字符串
var str = 'hello';
var o_str = new String('world');
console.log(typeof str,typeof o_str); //'string' 'object'
</script>属性length : 长度方法 (查、替、截、转)查indexOf('字符串',start) : 查找子串在父串中第一次出现的下标位置,如果没有,则返回-1var str = 'how do you do';
console.log(str.indexOf('do')); //4
console.log(str.indexOf('de')); //-1
console.log(str.indexOf('do',5)); //11
console.log(str.indexOf('do',4)); //42. lastIndexOf('字符串',start) : 查找子串在父串中从右向左查找第一次出现的下标位置,如果没有找到,返回 -1var str = 'how do you do';
console.log(str.indexOf('do')); //4
console.log(str.indexOf('de')); //-1
console.log(str.indexOf('do',5)); //11
console.log(str.indexOf('do',4)); //4
console.log(str.lastIndexOf('do')); //11
console.log(str.lastIndexOf('de')); //-1
console.log(str.lastIndexOf('do',5)); //4
console.log(str.lastIndexOf('do',4)); //43. charAt(index) : 根据下标查找字符。4. charCodeAt(index) : 根据下标查找字符编码。替replace(旧串,新串) : 替换字符串//声明字符串
var str = 'how do you do';
console.log(str.replace('do','de')); //'how de you do'
// for(var i = 0;i < str.length;i ++){
// str = str.replace('do','de');
// }
while(str.indexOf('do') !== -1){
str = str.replace('do','de');
}
console.log(str);截substring(start,end) : 从哪截取到哪,支持参数互换、不支持负数substr(start,length) :从哪截取多少个slice(start,end) :从哪截取到哪,不支持参数互换、支持负数var str = 'how do you do';
console.log(str.substring(4)); //do you do
console.log(str.substr(4)); //do you do
console.log(str.slice(4));//do you do
console.log(str.substring(4,6)); //do
console.log(str.substr(4,6)); //do you
console.log(str.slice(4,6));//do
console.log(str.substring(6,4)); //do
console.log(str.slice(6,4));//''
console.log(str.substring(-6,-4)); //''
console.log(str.slice(-6,-4));//'yo'转toUpperCase() : 将小写转为大写字母toLowerCase() : 将大写转为小写字母split('切割符',length) : 将字符串切割为数组 <script>
var str = 'How Are You';
//小写转为大写
console.log(str.toUpperCase()); //'HOW ARE YOU';
//大写转为小写
console.log(str.toLowerCase()); //'how are you'
//字符串转为数组
var arr = str.split(' ',5); //['How','Are','You']
console.log(arr);
console.log(str.split('o')); //['H','w Are Y','u']
console.log(str.split('')); //['H','o','w',' ','A','r','e',' ','Y','o','u']
console.log(str.split(' ',2)); //['How',"Are"];
console.log(str.split('oo')); //['How Are You'];
</script>var str = 'How Are You';
console.log(str.toUpperCase()); //HOW ARE YOU
console.log(str.toLowerCase()); //how are you
var arr = str.split(' ',2); //['How', 'Are']
console.log(arr);拼concat() : 合并字符串var str = 'hello';
console.log(str.concat(' world')); // hello world去空白trim() : 删除字符串两端空白var str = ' a b ';
console.log('(' + str.trim() + ')'); //(a b)2. trimStart() : 删除字符串左端空白var str = ' a b ';
console.log('(' + str.trimStart() + ')'); //(a b )3. trimEnd() : 删除字符串右端空白var str = ' a b ';
console.log('(' + str.trimEnd() + ')'); //( a b ) <script>
var str = ' a b ';
//去除所有空白
console.log('(' + str.replaceAll(' ','') + ')');
//去除左右两端的空白
console.log('(' + str.trim() + ')');
//去除左端空白
console.log('(' + str.trimStart() + ')');
//去除右端空白
console.log('(' + str.trimEnd() + ')');
</script>静态方法String.fromCharCode(编码) : 根据编码返回字符字符串操作相关案例去除字符串中所有的空格。 <script>
//去除字符串中所有的空格。
function fnRemoveAllSpace(str){
while(str.indexOf(' ') !== -1){
str = str.replace(' ','');
}
return str;
}
console.log('(' + fnRemoveAllSpace(' a b ') + ')');
</script>2. 去除字符串中左边的空格与右边的空格 “ a b “ “a b” (不能使用trim方法) <script>
//去除字符串中左边的空格与右边的空格 “ a b “ “a b” (不能使用trim方法)
function fnTrim(str){
//1. 去除左边的空白
while(str.charAt(0) === ' '){
str = str.replace(' ','');
}
//2. 去除右边的空白
var arr = str.split('');
// 找到数组中最后一个元素判断是否为空格
while(arr[arr.length - 1] === ' '){
arr.pop(); //删除数组中后面的元素
}
return arr.join('');
}
console.log('(' + fnTrim(' a b ') + ')');
</script>3. 统计出一个字符串中所有大写字母的数量与所有小写字母的数量 <script>
//统计出一个字符串中所有大写字母的数量与所有小写字母的数量
// ch >= 'A' && ch <= 'Z'
// ch >= 'a' && ch <= 'z'
function countNum(str){
//1. 准备两个变量,用于放置结果
var uppercase = 0;
var lowercase = 0;
//2. 遍历字符串
for(var i = 0,len = str.length;i < len;i ++){
//记录每一个字符
var ch = str.charAt(i);
//判断
if(ch >= 'A' && ch <= 'Z'){
uppercase ++;
}else if(ch >= 'a' && ch <= 'z'){
lowercase ++;
}
}
console.log('大写字母有:' + uppercase + '个\n小写字母有:' + lowercase + '个');
}
countNum('How Are You');
</script>4. 一次性输入多个成绩 <script>
//一次性输入多个成绩
//一、 获取页面元素
var o_txt = document.getElementById('txt');
var o_btn = document.getElementById('btn');
//二、实现功能
function fn(){
//1. 采集用户信息
// '1,2,3,4,5'
var info = o_txt.value;
//2. 转为数组
var arr = info.split(','); //['1','2','3','4','5']
//3. 求和
var sum = arr.reduce(function(prev,next){
return Number(prev) + Number(next);
})
//4. 将总成绩放到文本框中
o_txt.value = sum;
}
//三、添加事件
o_btn.onclick = fn;
</script>5. 影响职业因素<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>影响职业因素</h1>
<p>请输入一个单词: <input type="text" name="" id="txt"></p>
<p>结果: <span id="result"></span></p>
<p>
<input type="button" value="计算" id="submit">
<input type="button" value="清空" id="reset">
</p>
<script>
//影响职业因素,将单词中的每一个字母在字母表中的位置相加。如: abc 1 + 2 + 3 = 6
//一、获取页面元素
//1.1 获取单词的文本框
var o_txt = document.getElementById('txt');
//1.2 获取结果的容器
var o_result = document.getElementById('result');
//1.3 获取计算按钮
var o_submit = document.getElementById('submit');
//1.4 获取清空按钮
var o_reset = document.getElementById('reset');
//二、实现功能
//2.1 求职业因素的功能
function fnJob(){
//0. 准备变量,存储结果
var sum = 0;
var str = '';
//1. 采集用户信息
var info = o_txt.value; //'money'
//2. 字母表
var letter = ' abcdefghijklmnopqrstuvwxyz';
//3. 遍历用户单词
for(var i = 0,infoLen = info.length;i < infoLen;i ++){
//遍历字母表
for(j = 0,letterLen = letter.length;j < letterLen;j ++){
//进行比较
if(info.charAt(i) === letter.charAt(j)){
//求和
sum += j;
//求表达式
str += j + '+'; //1 + 2 + 3 +
}
}
}
//将结果放到页面中
// '1+2+3+' '1+2+3=6'
o_result.innerText = str.slice(0,-1) + '=' + sum;
}
//2.2 清空所有的框
function fnclear(){
o_txt.value = o_result.innerText = '';
}
//三、添加事件
o_submit.onclick = fnJob;
o_reset.onclick = fnclear;
</script>
</body>
</html>字符串的加密解密 <script>
//一、获取页面元素
//1.1 文本框
var o_txt = document.getElementById('txt');
//1.2 加密按钮
var o_secure = document.getElementById('secure');
//1.3 解密按钮
var o_unse = document.getElementById('unsecure');
//1.4 盒子
var o_box = document.getElementById('box');
//二、封装功能
//2.1 加密
function fnSecure(){
//0. 准备一个放置结果的字符串
var str = '';
//1. 采集加密的信息
var info = o_txt.value;
//2. 找到字符串中每一个字符(遍历),获取字符的编码值(charCodeAt) 进行 加一个数 或 减一个数 ,最后将改变后的编码值再转为字符
for(var i = 0,len = info.length;i < len;i ++){
str += String.fromCharCode(info.charCodeAt(i) + 88)
}
//3. 将加密后的内容放到页面中盒子中
o_box.innerText = str;
//4. 清空文本框中的内容
o_txt.value = '';
}
//2.2 解密
function fnUnSe(){
//0. 准备一个解密的的字符串
var str = '';
//1. 采集已加密后的信息
var info = o_box.innerText;
//2. 遍历出每一个加密后的字符,进行还原
for(var i = 0,len = info.length;i < len;i ++){
str += String.fromCharCode(info.charCodeAt(i) - 88);
}
//3. 将解密后的内容放到文本框中
o_txt.value = str;
//4. 清空盒子中的内容
o_box.innerText = '';
}
//三、添加事件
//3.1 加密按钮添加点击事件
o_secure.onclick = fnSecure;
//3.2 解密按钮添加点击事件
o_unse.onclick = fnUnSe;
</script>小结JavaScript中有许多常用的字符串方法,它们可以帮助你对字符串进行各种操作。以下是一些常见的字符串方法及其简要说明:length用于获取字符串的长度。charAt(index)返回指定索引位置的字符。charCodeAt(index)返回指定索引位置的字符的 Unicode 值。concat(str1, str2, ...)用于连接两个或多个字符串。indexOf(substring, start)返回指定子字符串第一次出现的位置,可指定搜索的起始位置。lastIndexOf(substring, start)返回指定子字符串最后一次出现的位置,可指定搜索的起始位置。slice(start, end)提取字符串的一部分,并返回一个新的字符串,可以指定开始和结束的位置。substring(start, end)类似于slice,不同之处在于当start大于end时会交换参数位置。substr(start, length)从起始索引位置提取指定长度的字符串。toUpperCase()将字符串转换为大写。toLowerCase()将字符串转换为小写。trim()去除字符串两端的空白字符。split(separator)将字符串分割成子字符串数组,可指定分隔符。replace(searchValue, replaceValue)替换字符串中的指定值。match(regexp)检索字符串中指定值,返回匹配的字符串数组。这些方法只是 JavaScript 中可用的众多字符串方法中的一部分。通过灵活运用这些方法,你可以在处理字符串时更加高效和便捷。
Ned
用Three.js搞个3D金字塔
1.金字塔金字塔组成:顶部一个四棱锥体,下面是上窄下宽依次递增的四棱台。那么需要计算一下依次下来的每个面大小,上面的形状底部作为下面形状的顶面,这样就每层都能看起来连续。 let top = idx * width;//顶面正方形宽度
let bottom = (idx + 1) * width;//底面正方形宽度
let geometry = new THREE.CylinderGeometry(
isDown ? bottom : top,
isDown ? top : bottom,
height,
4,//四棱台
4
);
//对应形状的需要放置的高度
//this.intervalH = 0.3 形状与形状之间的间隔距离
let y =
(isDown ? idx + 1 : list.length - idx) * height -
(isDown ? -idx : idx) * height * this.intervalH;
当顶面宽度为0时即为四灵锥。当顶面宽底面窄时,即可形成倒金字塔2.金字塔立体化可以看到,单纯的一种颜色,让金字塔看起来不太立体,这时候需要给每个面设置同色系的相近颜色,通过每个面的颜色差来达到这种效果。四棱台有六个面,四棱椎有五个面,那么就取五颜色值,侧面的四个面分别一个颜色,顶面和底面一个颜色。/**
* 获取暗色向渐变颜色
* @param {string} color 基础颜色
* @param {number} step 数量
* @returns {array} list 颜色数组
*/
export function getShadowColor(color, step) {
let c = getColor(color);
let { red, blue, green } = c;
console.log('%ccolor', `background:${color}`);
const s = 0.8;
const r = parseInt(red * s),
g = parseInt(green * s),
b = parseInt(blue * s);
console.log('%cshadow', `background:rgb(${r},${g}, ${b})`);
const rr = (r - red) / step,
gg = (g - green) / step,
bb = (b - blue) / step;
let list = [];
for (let i = 0; i < step; i++) {
list.push(
`rgb(${parseInt(red + i * rr)},${parseInt(green + i * gg)},${parseInt(blue + i * bb)})`
);
}
return list;
}
//获取颜色系
export function getDrawColors(cs, cLen) {
let list = [];
for (let i = 0; i < cs.length; i++) {
list.push(getShadowColor(cs[i], cLen));
}
return list;
}
this.cLen=5;
this.colors = getDrawColors(that.colors, this.cLen);给每个面的三角形设置对应材质索引materialIndex,注意,四棱椎与四棱台的面数是不同的,但画面的顺序是相同的,先画侧面,再画顶面和底面if (idx == 0) {
//四棱椎
geometry.faces.forEach((f, fIdx) => {
if (fIdx < 28) {
geometry.faces[fIdx].materialIndex = parseInt(fIdx / 7);
} else {
geometry.faces[fIdx].materialIndex = 3;
}
});
} else {
//四棱台
geometry.faces.forEach((f, fIdx) => {
if (fIdx < 32) {
geometry.faces[fIdx].materialIndex = parseInt(fIdx / 8);
} else {
geometry.faces[fIdx].materialIndex = 4;
}
});
}
let cs = this.colors[idx % this.colors.length];
let ms = [];
for (let k = 0; k < cs.length; k++) {
ms.push(getBasicMaterial(THREE, cs[k]));
}
let mesh = new THREE.Mesh(geometry, ms);四棱椎每个侧面有7个三角形组成,前28个三角形都是侧面。四棱台每个侧面有8个三角形组成,前32个三角形是侧面这样看起来终于有了3D的效果了。3.加上精灵文本/**
* 生成文本canvas
* @param {array} textList [{text:文本,color:文本颜色}]
* @param {number} fontSize 字体大小
* @returns
*/
export function getCanvasTextArray(textList, fontSize) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = fontSize + 'px Arial';
let textLen = 0;
textList.forEach((item) => {
let w = ctx.measureText(item.text + '').width;
if (w > textLen) {
textLen = w;
}
});
canvas.width = textLen;
canvas.height = fontSize * 1.2 * textList.length;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = fontSize + 'px Arial';
textList.forEach((item, idx) => {
ctx.fillStyle = item.color;
ctx.fillText(item.text, 0, fontSize * 1.2 * idx + fontSize);
});
return canvas;
}
/**
*生成多行文本精灵材质
* @param {THREE.js} THREE
* @param {array} textlist 文本颜色数组
* @param {number} fontSize 字体大小
* @returns
*/
export function getTextArraySprite(THREE, textlist, fontSize) {
//生成五倍大小的canvas贴图,避免大小问题出现显示模糊
const canvas = getCanvasTextArray(textlist, fontSize * 5);
return { ...getCanvaMat(THREE, canvas, 0.1), canvas };
}
let textList = [
{ text: item.name, color: fontColor },
{ text: item.value + '', color: fontColor }
];
const { mesh: textMesh } = getTextArraySprite(THREE, textList, height * 0.5);
textMesh.material.depthTest = false;
textMesh.name = 'f' + idx;
textMesh.position.z = idx == 0 ? width : (idx + 0.5) * width;
textMesh.position.y = y;
textMesh.position.x = 0;
this.objGroup.add(textMesh);canvas文本贴图一定要放大倍数,否则会出现近看模糊4.使用 var myPyramid = new MyPyramid();
window.myPyramid = myPyramid;
myPyramid.initThree(document.getElementById('canvas'));
myPyramid.createChart({
//颜色
colors: ['#fcc02a', '#f16b91', '#187bac'],
//数据
data: [
{ name: '小学', value: 100 },
{ name: '中学', value: 200 },
{ name: '大学', value: 300 }
],
//是否倒金字塔
isDown: false,
//每层高度
pHeight: 40,
//递增宽度
pWidth: 20,
//字体颜色
fontColor: 'rgb(255,255,255)',
//相机位置
cameraPos: {
x: 178.92931795958233,
y: 210.63511436762354,
z: 357.5498605603872
},
//控制器位置
controlsPos: {
x: -4.895320674125236,
y: 27.139140036227758,
z: 1.5576536521931232
}
});5.渐变3D金字塔可能上面的效果有点不够设计感,那么就来点好看的渐变色吧!获取一个比当前颜色浅的亮色 getLightColor(color) {
let c = getColor(color);
let { red, blue, green } = c;
console.log('%ccolor', `background:${color}`);
const l = 0.5;
const r = red + parseInt((255 - red) * l),
g = green + parseInt((255 - green) * l),
b = blue + parseInt((255 - blue) * l);
console.log('%clight', `background:rgb(${r},${g}, ${b})`);
return `rgb(${r},${g}, ${b})`;
}渐变shaderuniform vec3 topColor; //顶面颜色
uniform vec3 bottomColor;//底面颜色
varying vec2 vUv;
varying vec3 vNormal;
void main() {
if(vNormal.y==1.0){//顶面
gl_FragColor = vec4(topColor, 1.0 );
}else if(vNormal.y==-1.0){//底面
gl_FragColor = vec4(bottomColor, 1.0 );
}else{//根据uv的y坐标混合两种颜色,形成渐变
gl_FragColor = vec4(mix(bottomColor,topColor,vUv.y), 1.0 );
}
}获取渐变ShaderMaterial
export function getGradientShaderMaterial(THREE, topColor, bottomColor) {
const uniforms = {
topColor: { value: new THREE.Color(getRgbColor(topColor)) },
bottomColor: { value: new THREE.Color(getRgbColor(bottomColor)) }
};
return new THREE.ShaderMaterial({
uniforms: uniforms,
vertexShader: vertexShader,
fragmentShader: barShader,
side: THREE.DoubleSide
});
}将材质换成渐变材质let cs = that.colors[idx % that.colors.length];
let ms = getGradientShaderMaterial(THREE, this.getLightColor(cs), cs);
let mesh = new THREE.Mesh(geometry, ms);Github地址https://github.com/xiaolidan00/my-three
Ned
用Three.js搞个炫酷3D字体
点进来就看炫酷的3D字体!1.准备工作:字体模型json可以通过facetype.js将字体包转成typeface.jsonfacetype.jsgithub链接:https://github.com/gero3/facetype.js facetype.js官网链接:http://gero3.github.io/facetype.js/2.加载字体if (!this.loader) {
this.loader = new THREE.FontLoader();
}
this.loader.load('DIN_Bold.json', (font) => {
//...
});3.创建字体并获取字体点面 //创建字体图形
let geometry = new THREE.TextGeometry(that.text, {
font: font,
size: that.fontSize, //字体大小
height: that.thickness, //字体厚度
curveSegments: 3,
bevelThickness: 2,
bevelSize: 1,
bevelEnabled: true
});
//图形居中
geometry.center();
//转为缓存图形,方便获取点面等数据
geometry = new THREE.BufferGeometry().fromGeometry(geometry);
//三角形面数
const numFaces = geometry.attributes.position.count / 3;4.字体形状赋值颜色 //需要赋值的点颜色数组
const colors = new Float32Array(numFaces * 3 * 3);
const color = new THREE.Color();
for (let f = 0; f < numFaces; f++) {
const index = 9 * f;
//随机颜色
color.setRGB(
Math.random() * 0.5 + 0.5,
Math.random() * 0.5 + 0.5,
Math.random() * 0.5 + 0.5
);
for (let i = 0; i < 3; i++) {
//给3个点赋值同样的颜色,形成一个颜色的三角形
colors[index + 3 * i] = color.r;
colors[index + 3 * i + 1] = color.g;
colors[index + 3 * i + 2] = color.b;
}
}
//设置顶点着色器值
geometry.setAttribute('aColor', new THREE.BufferAttribute(colors, 3));5.着色器材质5.1 顶点着色器 attribute vec3 aColor;
varying vec3 vColor;
void main() {
vColor = aColor;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}5.2 片元着色器 varying vec3 vColor;
void main() {
gl_FragColor =vec4(vColor, 1.0 );
}5.3 着色材质//着色器材质
const shaderMaterial = new THREE.ShaderMaterial({
uniforms: {
amplitude: { value: 0.0 }
},
vertexShader: document.getElementById('vertexshader').textContent,
fragmentShader: document.getElementById('fragmentshader').textContent
});6.使用 var myFont = new MyFont();
myFont.initThree(document.getElementById('font'));
myFont.createChart({
text: '666',//文本
fontSize: 20,//字体大小
thickness: 5,//厚度
distance: 30,//偏移距离
minDistance: 5,//最小偏移距离
});7.字体每个面的偏移顶点着色器:normal是three顶点着色器默认值 attribute vec3 aColor;
varying vec3 vColor;
void main() {
vColor = aColor;
vec3 newPosition =position+normal*2.0;//每个面偏移
gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );
}8.让每个面三角形随机偏移 attribute vec3 aColor;
attribute vec3 displacement; //偏移值
varying vec3 vColor;
void main() {
vColor = aColor;
vec3 newPosition =position+normal*displacement;//每个面随机偏移
gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );
}设置随机偏移值 //需要赋值的偏移变量数组
const displacement = new Float32Array(numFaces * 3 * 3);
for (let f = 0; f < numFaces; f++) {
const index = 9 * f;
//随机偏移值
const d = that.minDistance + that.distance * Math.random();
for (let i = 0; i < 3; i++) {
//给3个点赋值偏移值,形成对应的三角形
displacement[index + 3 * i] = d;
displacement[index + 3 * i + 1] = d + Math.random() * that.minDistance;
displacement[index + 3 * i + 2] = d + Math.random() * that.minDistance;
}
}
//设置顶点着色器值
geometry.setAttribute('displacement', new THREE.BufferAttribute(displacement, 3)); 9.随机偏移的三角形汇聚成原来的字体9.1 偏移uniform float time;
attribute vec3 aColor;
attribute vec3 displacement; //偏移值
varying vec3 vColor;
void main() {
vColor = aColor;
vec3 newPosition =position+normal*displacement*time;//每个面随机偏移随时间变
gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );
}9.2 汇聚让偏移值随时间递减到0,汇聚成原来的字体,则时间值递减 animateAction() {
if (this.mesh) {
if (this.time >= 0) {
this.time += 0.005;
this.mesh.material.uniforms.time.value = 2.0 - this.time;
if (this.time >= 2.0) {
this.time = -1;
}
} else {
this.mesh.material.uniforms.time.value = 0.0;
}
}
}10.字体面数细化 import { TessellateModifier } from '../node_modules/three/examples/jsm/modifiers/TessellateModifier.js';
//细化修改器
const tessellateModifier = new TessellateModifier(8, 6);
//修改图形的面数
geometry = tessellateModifier.modify(geometry);细化修改器 TessellateModifier( maxEdgeLength=0.1 最大边长, maxIterations = 6 最大遍历次数, maxFaces = Infinity 最大面数 )可以看到相对于原来的字体,面数增加了,更加精细了!11.同理可在其他模型形状也可使用该效果
let geometry = new THREE.CylinderGeometry(that.topRadius, that.bottomRadius, that.height);
//图形居中
geometry.center();
//细化修改器
const tessellateModifier = new TessellateModifier(8, 6);
//修改图形的面数
geometry = tessellateModifier.modify(geometry);
//转为缓存图形,方便获取点面等数据
geometry = new THREE.BufferGeometry().fromGeometry(geometry);
//...GitHub地址http://github.com/xiaolidan00/my-three参考:https://threejs.org/examples/#webgl_modifier_tessellation
Ned
解锁JavaScript正则表达式的奥秘:掌握高效的文本处理技巧
正则表达式概述什么是正则表达式?检测字符串的一组规则。2.正则表达式的作用?主要用于表单验证 处理复杂的字符串。如何声明正则表达式?字面量方式:/正则表达式/标志位2.构造函数方式:new RegExp('正则表达式','标志位')正则表达式的方法?正则表达式.test(字符串) : 检测字符串中是否包含了正则的内容,如果包含了,则返回true,否则,返回false.正则表达式.exec(字符串) : 将字符串中匹配到正则的内容以数组的方式返回。如果没有匹配的内容,返回null处理正则表达式的字符串方法?字符串.match(正则) : 将字符串中匹配到正则的内容以数组的方式返回。如果没有匹配的内容,返回null字符串.search(正则) : 与indexOf的功能相同,查找正则匹配的内容在父串中第一次出现的下标位置 ,如果没有找到,返回 -1var re = /de/;
var str = 'how do you do';
console.log(str.search(re)); //-1exec与match的区别://1. 无g无组的情况
var re = /do/;
var str = 'how do you do';
console.log(re.exec(str)); //['do']
console.log(str.match(re)); //['do']
//2. 有g无组的情况
var re = /do/g;
var str = 'how do you do';
console.log(re.exec(str)); //['do']
console.log(str.match(re)); //['do','do']
//3. 无g有组的情况
var re = /([a-z]+) (\d+)/;
var str = 'haha 2022';
console.log(re.exec(str)); //['haha 2022','haha','2022']
console.log(str.match(re)); //['haha 2022','haha','2022']
//4. 有g有组的情况
var re = /([a-z]+) (\d+)/g;
var str = 'haha 2022,hehe 2023';
console.log(re.exec(str)); //['haha 2022','haha','2022']
console.log(str.match(re)); //['haha 2022', 'hehe 2023']正则表达式的元字符 (三、三、二、三个一)三: {} [] (){} : 表示{}前面的一个或一组字符连续出现的次数。{m} : 表示{}前面的一个或一组字符连续出现 m 次var str = 'ooo';
var re = /^o{3}$/;
console.log(re.test(str)); //true{m,} : 表示{}前面的一个或一组字符连续出现 m 至 无限次,至少出现m次var str = 'ooooooooooooooooooooo';
var re = /^o{3,}$/;
console.log(re.test(str)); //true{m,n} : 表示{}前面的一个或一组字符连续出现 m 至 n次。var str = 'oooo';
var re = /^o{3,5}$/;
console.log(re.test(str)); //true2. [] : 表示取值范围var re = /^[abc]{2,5}$/;
var str = 'aaaa';
console.log(re.test(str));
var re = /^[a-z]{2,5}$/;
var str = 'what';
console.log(re.test(str));
var re = /^[a-z]{2,5}$/i;
var str = 'What';
console.log(re.test(str));
var re = /^[a-zA-Z]{2,5}$/;
var str = 'What';
console.log(re.test(str));
var re = /^[0-9]{6}$/;
var str = '666888';
console.log(re.test(str));
var re = /^[a-zA-Z0-9_]{8,16}$/
var str = 'abc_123456';
console.log(re.test(str));
var re = /^[\u4e00-\u9fa5]{3,}$/
var str = '张小三';
console.log(re.test(str));3. () : 表示组var re = /^(中国){2,}$/
var str = '中国中国';
console.log(re.test(str));三: * + ?* : 表示*号前面的一个或一组字符连续出现 0 至 无限次 {0,}var re = /^do*$/
var str = 'd';
console.log(re.test(str));2. + : 表示+号前面的一个或一组字符连续出现 1 至 无限次 {1,}var re = /^do+$/
var str = 'doooooooooooo';
console.log(re.test(str));3. ? : 表示?号前面的一个或一组字符连续出现 0 至 1 次 {0,1}var re = /^do?$/
var str = 'd';
console.log(re.test(str));二: ^ $^写在正则表达式的开头,表示断头,限制字符串的开头必须是指定的字符。 写在[]的开头,表示取反var re = /^[^0-9]+/
var str = 'd45678';
console.log(re.test(str));
var re = /^[^0-9]+$/
var str = 'd!@#$%';
console.log(re.test(str));2.$表示断尾,限制字符串的结尾必须是指定的字符。var re = /^h.*w$/;
var str = 'h2345r6t7y8uiokjfder56789iokw';
console.log(re.test(str));三个一 : . | \. : 表示模糊匹配任意一个字符。var re = /^h.*w$/;
var str = 'h2345r6t7y8uiokjfder56789iokw';
console.log(re.test(str));2. | : 表示或,一般结合组一起使用。var re = /^(男|女)$/;
var str = '男';
console.log(re.test(str));3. \ : 表示转义符,将具有特殊含义的符号转为普通字符。//文件类型(后缀)(扩展名) .html .js .css .mp4 .c
var re = /\.[a-zA-Z0-9]{1,4}$/;
var str = 'index.html';
console.log(re.test(str));\d : 表示数字 [0-9]//邮政编码
var re = /^\d{6}$/;
var str = '034017';
console.log(re.test(str));
//手机号
var re = /^1(1|2|3|4|5|6|7|8|9)\d{9}$/;
var str = '17710875717';
console.log(re.test(str));
// 2022/7/20
var re = /^(\d{4}|\d{2})\/\d{1,2}\/\d{1,2}$/;
var str = '22/7/20';
console.log(re.test(str));\D : 表示非数字 [^0-9] \w : 表示字母、数字、下划线 [a-zA-Z_]//密码规则
var re = /^\w{8,16}$/;
var str = 'abc_12345678';
console.log(re.test(str));\W : 表示 非(字母、数字、下划线) \s : 表示空白 \S : 表示非空白 \b : 表示单词边界 \B : 表示非边界正则表达式的标志位g : 全局匹配,匹配出所有的匹配项,并非在发现第一个匹配项时就立即停止i : 不区分大小写字母m: 表示多行模式(multiline),可以进行多行匹配,但是只有使用^和$模式时才起作用,在其他模式中,加不加入m都可以进行多行匹配案例分析var pattern = /(google){4,8}/;
var str = 'googlegoogle';
alert(pattern.test(str));
var pattern = /(google|baidu|bing)/;
var str = 'google';
alert(pattern.test(str));
var pattern = /goo\sgle/;
var str = 'goo gle';
alert(pattern.test(str));
var pattern = /google\b/;
var str = 'google';
var str2= 'googleaa googlexx google dsdddd';
alert(pattern.test(str));
alert(pattern.test(str2));
var pattern = /g\w*gle/;
var str = 'google';
alert(pattern.test(str));
var pattern = /google\d*/;
var str = 'google444';
alert(pattern.test(str));
var pattern = /\D{7,}/;
var str = 'google8';
alert(pattern.test(str));
var pattern = /g[a-zA-Z_]*gle/;
var str = 'google';
alert(pattern.test(str));
var pattern = /g[^0-9]*gle/;
var str = 'google';
alert(pattern.test(str));
var pattern = /[a-z][A-Z]+/;
var str = 'gOOGLE';
alert(pattern.test(str));
var pattern = /g.*gle/;
var str = 'google';
alert(pattern.test(str));
var pattern = /^[a-z]+\s[0-9]{4}$/i;
var str = 'google 2012';
alert(pattern.exec(str));
var pattern = /^[a-z]+/i;
var str = 'google 2012';
alert(pattern.exec(str));
var pattern = /^([a-z]+)\s([0-9]{4})$/i;
var str = 'google 2012';
alert(pattern.exec(str)[0]);
alert(pattern.exec(str)[1]);
alert(pattern.exec(str)[2]);
var pattern = /Good/ig;
var str = “good good study!,day day up!”;
alert(str.replace(pattern,’hard’));
var pattern = /(.*)\s(.*)/;
var str = 'google baidu';
var result = str.replace(pattern, '$2 $1');
document.write(result)
var pattern = /8(.*)8/;
var str = 'This is 8google8';
var result = str.replace(pattern,'<strong>$1</strong>');
document.write(result);
Var pattern = /good/ig;
var str = ‘good good study!,day day up!’;
alert(str.match(pattern));
alert(str.match(pattern).length);
var pattern = /8(.*)8/;
var str = 'This is 8google8, dd 8ggg8';
alert(str.match(pattern));
alert(RegExp.$1);
var pattern = /good/ig;
var str = ‘good good study!,day day up!’;
alert(str.search(pattern));手机号检测Xxxx 年 xx 月 xx 日12/25/2016用户名由 3-10 位的字母下划线和数字组成。不能以数字或下划线开头。只能已字母开头。允许全部是字母字符串过滤(首尾空格)(扩展)至少有两种(字母、数字、下划线、!@#.)6 到 12 位/^(?![\d]+$)(?![a-zA-Z]+$)(?![\W]+$)[\da-zA-Z_!@#\.]{6,12}$/
Ned
JavaScript概述
什么是javascript?是一门(基于对象)和(事件驱动)的(脚本语言)。2. js诞生于哪一年?哪个公司?谁?第一个名字叫什么?1995年 网景 布兰登 liveScript3. JS的第一套标准:ECMA-2624. js包含哪些部分?ECMAScript (核心)BOMDOM : Document Object Model (文档对象模型)5. java和javascript是什么关系?没有任何关系。6. js的作用是什么?实现页面特效实现交互JS的三种书写方式行内式内嵌式 <script type="text/javascript"></script>外链式 <script src=''></script>注 : 外部引用脚本时,script标签中不要写任何内容,写了也不执行!如何输出?alert() : 以警告框的方式输出,缺点是会阻止后面语句的执行。如果括号中是数字就不须要加引号如果是其它非数字数据要加引号(不区分单引号或双引号),在JS中只要加上引号就叫做字符串。引号嵌套时要注意不能交叉使用引号 ' "" ' " '' "JS语言中,所有的标点符号都是英文半角状态document.write() : 在页面中输出,缺点是会影响现有布局。console.log() : 在控制台中输出,会输出对象的详细信息。JS中的换行页面中 <br>非页面中 \n : 换行 \t : 横向跳格(一次八个空格) \ : 转义符,将具有特殊含义的符号转成普通字符JS中的注释单行注释 //注释内容多行注释 /*注释内容、多行注释、段落注释、块注释*/JS中的数据类型基本数据类型Number (数字类型) 3 3.3 -4String (字符串) '' ""Boolean (布尔) true falsenull (表示空的对象)undefined (未定义)复合数据类型Object (对象)JS中的命名规则只能包含(字母、数字、下划线、$)不能以数字开头不能是关键字或保留字要语义化驼峰命名大驼峰 : HowAreYou 常用于 类名或构造函数名小驼峰 : howAreYou 常用于 函数名 how_are_you 常用于 变量名匈牙利命名法整数 int i_num 以i开头的变量存储整数小数 float f_num 以f开头的变量存储小数字符 char ch 以c开头的变量存储一个字符字符串 String str 以s开头的变量存储一个字符串布尔 Boolean bool 以b或bo开头的变量存储布尔变量对象 Object o_div 以o开头的变量存储对象数组 array arr 以arr开头的变量存储数组函数 function fn 以fn开头的变量存储函数JS 变量用于在内存中开辟空间,这个空间中存储数据,且随着程序的运行,空间中的数据会发生变化,所以称为变量。声明变量显式声明 : var 变量名,变量名,变量名;隐式声明 : 变量名 = 值;给变量赋值初始化变量: 在声明变量的同时给它赋值。先声明变量,再赋值。注意:一个变量名只能存储一个值当再次给一个变量赋值的时候,前面一次的值就没有了变量名称区分大小写(JS 严格区分大小写)JS 运算符自增自减运算符 ++ --规则:从左到右,如果遇到 变量 ++ 先取出变量中的值参与其它运算,再自加1。 如果遇到 ++ 变量,直接取加1后的值参与运算。//声明一个变量
// var i = 3;
// 3 4
// // i ++;
// ++ i;
// console.log(i); //4
// var i = 3;
// // 3 4
// console.log(i ++); //3
// console.log(i); //4
// var i = 3;
// // 4
// console.log(++ i); //4
// console.log(i); //4
// var i = 3;
// // 4 4 5 5 4
// var j = ++ i + i ++ + i --;
// // 4 + 4 + 5
// console.log(i,j); //4 13
var i = 3;
// 3 4 5 5 6
var j = i ++ + ++i + i ++;
// 3 + 5 + 5 j = 13,i = 6
// 7 13 14 7 6
var k = ++i + j ++ + i --;
// 7 + 13 + 7 k = 27 j = 14 i = 6
console.log(i,j,k); //6算术运算符* (乘) /(除) %(模) -(减)规则:Number类型,直接运算Number与String类型运算,先将String类型转为Number类型,如果是纯数字字符串,则转为数字,直接运算。如果不是纯数字字符串,则转为NaN,与NaN计算的结果,都是NaNtrue(1) false(0) null(0) undefined(NaN)console.log(2 * 3); //6
console.log(2 * '3'); //6
console.log('2' * '3'); //6
console.log(2 * '3a'); //NaN (Not a Number)
console.log(2 * true); //2
console.log(2 * false); //0
console.log(2 * null); //0
console.log(2 * undefined); //NaN
console.log(10 / 2); //5
console.log(10 / '2'); //5
console.log(10 / '2a'); //NaN
console.log(true / 2); //0.5
console.log(false / 2); //0
console.log(2 / null); //Infinity 无穷
console.log(0 / 0); //NaN
console.log(2 / undefined); //NaN
console.log(5 % 2); //1
console.log(5 % 5); //0
console.log(5 % '2'); //1
console.log(5 % '2a'); //NaN
console.log(5 % true); //0
console.log(5 % false); //NaN
console.log(2 % 5); //2
console.log(3 % 5); //3
console.log(5 - 3); //2
console.log(5 - '2'); //3
console.log(5 - '2a'); //NaN
console.log(5 - true); //4
console.log(5 - false); //5
console.log(5 - null); //5
console.log(5 - undefined); //NaN(加) 规则:如果+号两边有字符串,则将两个数据连接成新的字符串。true(1) false(0) null(0) undefined(NaN)关系运算符> (大于) <(小于) >= (大于或等于) <= (小于或等于)规则:Number类型之间比较,直接比较String类型之间比较,从左到右依次比较字符,直到比出结果。Number与String类型之间比较,先将String转为Number,再比较true(1) false(0) null(0) undefined(NaN)console.log(3 > 2); //true
console.log(1 >= '1'); //true
console.log(0 >= 'false') //false
console.log(0 <= false); //true
console.log(0 >= null); //true
console.log('100' > '2') //false
console.log(2 > NaN); //false== (等于) != (不等于) === (恒等于) !== (不恒等)区别: == 和 != : 只比较结果,不比较类型。 === 和 !== : 先比较类型,如果类型相同,再比较结果。console.log(2 == '2'); //true
console.log(2 === '2'); //false
console.log(2 != '2'); //false
console.log(2 !== '2'); //true//切记:切记:切记:
console.log(null == 0); //false
console.log(null == false); //false
console.log(null == ''); //false
console.log(null == undefined); //true
console.log(null === undefined); //false
console.log(NaN === NaN); //false逻辑运算符 (false/0/''/null/undefined/NaN)! : 非规则: 非真即假、非假即真console.log(!0); //true
console.log(!false); //true
console.log(!''); //true
console.log(!null); //true
console.log(!undefined); //true
console.log(!NaN); //true
console.log(!' '); //false
console.log(!8); //false
console.log(!-8); //false
console.log(!!8); //true&& : 与规则 如果&&左边表达式的值为true时,返回右边表达式的值。 如果&&左边表达式的值为false时,发生短路,返回左边表达式的值。var i = 3;
var j = !i >= 0 && (i = 4);
console.log(i,j); //4 4
var k = 3;
var m = k % 5 - 3 && (k = 4);
console.log(k,m); //3 0
var a = 3;
var b = 3 - 3 * 4 % 2 && (a = 4);
console.log(a,b); //4 4|| : 或规则 如果||左边表达式的值为true时,发生短路,返回左边表达式的值。 如果||左边表达式的值为false时,返回右边表达式的值。 var i = 3;
var j = i - i % 4 || (i = 4);
console.log(i,j); //4 4
var k = 3;
var m = 3 - 3 % 2 || ( k = 4);
console.log(k,m); //3 2
赋值运算符简单赋值 =赋值号左边,只能变量 赋值号右边,可以常量、变量、表达式。复合算术赋值 += -= *= /= %=规则:先取运算符左边变量中的值 与 右边表达式的值 进行相应的 算术运算,最后将运算的结果再赋值给左边的变量。特殊运算符new : 用于创建对象var i = 3; //Number
var o_num = new Number(3);
console.log(typeof i,typeof o_num); //'number' 'object'
var o_bool = new Boolean();
var o_str = new String();
var obj = new Object();typeof : 用于检测数据类型console.log(typeof 3); //'number'
console.log(typeof '3'); //'string'
console.log(typeof true); //'boolean'
console.log(typeof null); //'object' 不是对象的对象。
console.log(typeof undefined); //'undefined'
console.log(typeof NaN); //'number' 不是数字的数字
console.log(typeof typeof false);//'string'数据类型的转换自动类型转换强制类型转换parseInt('字符串',2~36进制范围) : 将字符串中开头有效的数字部分转为整数。当省略第二个参数或第二个参数为0时,表示十进制。 当第二个参数是< 2 || > 36时,结果为NaN 如果第二个参数是2~36中的范围时,判断第一个参数是否符合第二个参数的进制范围,如果符合,则正常转为整数,如果不符合进制范围,则直接返回NaNconsole.log(parseInt('1.2.3')); //1
console.log(parseInt('3 42')); //3
console.log(parseInt(' 3')); //3
console.log(parseInt('')); //NaN
console.log(parseInt('a23')); //NaN
console.log(parseInt('2',0)); //2
console.log(parseInt('2',1)); //NaN
console.log(parseInt('2',37)); //NaN
console.log(parseInt('2',2)); //NaN
console.log(parseInt('2',3)); //2parseFloat('字符串') : 将字符串中开头有效的数字部分转为小数。console.log(parseFloat('1.2.3.5')); //1.2
console.log(parseFloat('2 34')); //2
console.log(parseFloat('')); //NaN
console.log(parseFloat('a2.3')); //NaNNumber('字符串') : 将纯有效数字字符串转为数字。console.log(Number('1.2.3')); //NaN
console.log(Number('2 34')); //NaN
console.log(Number('')); //0
console.log(Number('a23')); //NaN
Ned
探秘JavaScript事件传播机制:冒泡、捕获与目标阶段解析
事件 传播1.浏览器内的事件流机制什么是事件的执行机制呢?思考一个问题?当一个大盒子嵌套一个小盒子的时候,并且两个盒子都有点击事件你点击里面的小盒子,外面的大盒子上的点击事件要不要执行当元素触发一个事件的时候,其父元素也会触发相同的事件,父元素的父元素也会触发相同的事件2.事件捕获 / 目标 / 冒泡事件捕获 : 从上到下、从祖先到子孙依次传递事件的过程事件目标 : 触发事件的对象,你是点击在哪个元素身上了,那么这个事件的 目标 就是什么事件冒泡 : 从下到上、从子孙到祖先依次传递事件的过程浏览器默认启动了事件冒泡!!!IE和欧朋浏览器不支持事件捕获。<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body{
border: 1px solid black;
}
#box{
width: 200px;
height: 150px;
background: pink;
}
.pox{
width: 100px;
height: 100px;
background: yellow;
}
</style>
</head>
<body>
<div id="box">
<div class="pox"></div>
</div>
<script>
//1. html
var html = document.documentElement;
//2. body
var body = document.body;
//3. box
var box = document.querySelector('#box');
//4. pox
var pox = document.querySelector('.pox');
document.onclick = function(){
alert('document');
}
html.onclick = function(){
alert('html');
}
body.onclick = function(){
alert('body');
}
box.onclick = function(){
alert('box');
}
pox.onclick = function(evt){
var e = evt || window.event;
//标准浏览器:阻止事件冒泡
// e.stopPropagation();
//IE浏览器:阻止事件冒泡
// e.cancelBubble = true;
//兼容
e.stopPropagation ? e.stopPropagation() : e.cancelBubble = true;
alert('pox');
}
</script>
</body>
</html>阻止事件传播<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body{
border: 1px solid black;
}
#box{
width: 200px;
height: 150px;
background: pink;
}
.pox{
width: 100px;
height: 100px;
background: yellow;
}
</style>
</head>
<body>
<div id="box">
<div class="pox"></div>
</div>
<script>
//1. html
var html = document.documentElement;
//2. body
var body = document.body;
//3. box
var box = document.querySelector('#box');
//4. pox
var pox = document.querySelector('.pox');
document.onclick = function(){
alert('document');
}
html.onclick = function(){
alert('html');
}
body.onclick = function(){
alert('body');
}
box.onclick = function(){
alert('box');
}
pox.onclick = function(evt){
var e = evt || window.event;
//标准浏览器:阻止事件冒泡
// e.stopPropagation();
//IE浏览器:阻止事件冒泡
// e.cancelBubble = true;
//兼容
e.stopPropagation ? e.stopPropagation() : e.cancelBubble = true;
alert('pox');
}
</script>
</body>
</html>标准浏览器: event.stopPropagation() IE浏览器: event.cancelBubble = true; 兼容://阻止事件冒泡的兼容
function stopPropagation(evt){
var e = evt || window.event;
e.stopPropagation ? e.stopPropagation() : e.cancelBubble = true;
}默认行为默认行为,就是不用我们注册,它自己就存在的事情比如我们点击鼠标右键的时候,会自动弹出一个菜单比如我们点击 a 标签的时候,我们不需要注册点击事件,他自己就会跳转页面...这些不需要我们注册就能实现的事情,我们叫做 默认事件阻止浏览器默认行为标准浏览器: event.preventDefault() IE浏览器: event.returnValue = false 兼容://阻止浏览器默认行为的兼容
function preventDefault(evt){
var e = evt || window.event;
e.preventDefault ? e.preventDefault() : e.returnValue = false;
}return false : 既阻止默认行为,也阻止事件冒泡! <script>
//获取ul
var ul = document.querySelector('ul');
//右键菜单事件 oncontextmenu
document.oncontextmenu = function(evt){
var e = evt || window.event;
//标准浏览器:阻止默认行为
// e.preventDefault();
//IE浏览器:阻止默认行为
// e.returnValue = false;
//兼容
e.preventDefault ? e.preventDefault() : e.returnValue = false;
// alert('右键菜单已禁用!');
ul.style.display = 'block';
ul.style.left = e.pageX + 'px';
ul.style.top = e.pageY + 'px';
}
document.onclick = function(){
ul.style.display = 'none';
}
</script> <script>
//获取a
var a = document.querySelector('a');
//点击事件
a.onclick = function(event){
// e = event || window.event;
// e.preventDefault ? e.preventDefault() : e.returnValue = false;
return false; // 既阻止默认行为,也阻止事件冒泡
}
</script>事件委托就是把我要做的事情委托给别人来做因为我们的冒泡机制,点击子元素的时候,也会同步触发父元素的相同事件所以我们就可以把子元素的事件委托给父元素来做将加到子元素上的事件,添加到父元素上,为了提高性能。原理是利用了事件冒泡。实现事件委托<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<h4>hhhhhhhhhhhh</h4>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<p>pppppppppppp</p>
<li>8</li>
<li>9</li>
<li>10</li>
</ul>
<script>
// //获取所有的li
// var li = document.querySelectorAll('ul>li');
// //遍历,添加事件
// for(var i = 0,len = li.length;i < len;i ++){
// li[i].onclick = function(){
// alert(this.innerText);
// }
// }
//获取ul
var ul = document.querySelector('ul');
//添加事件
ul.onclick = function(evt){
var e = evt || window.event;
//获取事件源
//标准浏览器
// var target = e.target;
//IE浏览器
// var target = e.srcElement;
var target = e.target || e.srcElement;
//过滤
if(target.nodeName === 'LI'){
alert(target.innerText);
}
// alert(this.innerText);
}
var o_li = document.createElement('li');
o_li.innerText = 11;
ul.appendChild(o_li);
</script>
</body>
</html>事件添加到父元素 通过事件对象获取事件源 进行过滤事件源<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<h4>hhhhhhhhhhhh</h4>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<p>pppppppppppp</p>
<li>8</li>
<li>9</li>
<li>10</li>
</ul>
<script>
// 已知页面上有结构ul,内有10个li,每个li的内容不同,请使用事件委托的方式给每个li都绑定点击事件,点击的时候打印对应li的内容
//获取ul
var ul = document.querySelector('ul');
//添加事件
ul.onclick = function(evt){
var e = evt || window.event;
//获取事件源
//标准浏览器
// var target = e.target;
//IE浏览器
// var target = e.srcElement;
var target = e.target || e.srcElement;
//过滤
if(target.nodeName === 'LI'){
alert(target.innerText);
}
// alert(this.innerText);
}
var o_li = document.createElement('li');
o_li.innerText = 11;
ul.appendChild(o_li);
</script>
</body>
</html>targettarget 这个属性是事件对象里面的属性,表示你点击的目标当你触发点击事件的时候,你点击在哪个元素上,target 就是哪个元素这个 target 也不兼容,在 IE 下要使用 srcElement标准浏览器: event.target IE浏览器: event.srcElement// 获取事件源的兼容
function getTarget(evt){
var e = evt || window.event;
return e.target || e.srcElement;
}封装事件库<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#box{
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<div id="box"></div>
<script>
//callBack : 回调
function bindEvent(dom,tapCB,clickCB){
//记录触摸开始的时间
var startTime = 0;
//是否移动了手指
var isMove = false;
//监听触摸开始事件
dom.addEventListener('touchstart',function(){
//当前时间
startTime = Date.now();
})
dom.addEventListener('touchmove',function(){
isMove = true;
})
dom.addEventListener('touchend',function(){
//如果触摸的时间差 <= 150 && 移动了
if(Date.now() - startTime <= 150 && isMove){
//轻触
if(tapCB instanceof Function){
tapCB();
}
}else{
//点击
if(typeof clickCB === 'function'){
clickCB();
}
}
})
}
//获取div
var div = document.querySelector('#box');
bindEvent(div,function(){
console.log('我摸了一下');
},function(){
console.log('我点击了一个div');
})
</script>
</body>
</html>移动端tap事件// 封装事件
// dom : 事件源,触发事件的对象
// tapCallback : 轻击后的回调函数,轻击后想要执行什么?
// clickCallback : 点击后的回调函数,点击后想要执行什么?
var bindTapEvent = function(dom, tapCallback, clickCallback) {
// 声明变量-开始时间
var startTime = 0;
// 声明变量-记录是否移动-默认为false,没有移动
var isMove = false;
//监听触摸开始事件
dom.addEventListener('touchstart', function(e) {
//记录触摸后的时间
startTime = Date.now()
});
//监听触摸移动事件
dom.addEventListener('touchmove', function(e) {
//移动后,记录为true
isMove = true
});
//监听触摸结束事件
dom.addEventListener('touchend', function(e) {
//检测触摸时间 与 是否移动
if ((Date.now() - startTime) < 150 && isMove) {
// 假设点击的时间间隔小于150ms为轻击事件
tapCallback && tapCallback.call(this, e)
} else {
// 假设点击的时间间隔大于150ms为点击事件
clickCallback && clickCallback.call(this, e)
}
//开始时间恢复为0
startTime = 0;
//记录移动为false
isMove = false;
});
}移动端左滑右滑事件/*
Touch事件:
touches:当前位于屏幕上的所有手指的一个列表
targetTouches:位于当前DOM元素上的手指的一个列表;
changedTouches:涉及当前事件的手指的一个列表;
screenX,screenY:触摸点相对于屏幕上边缘的坐标;
clientX,clientY:触摸点相对于浏览器的viewport左边缘的坐标,不包括左边的滚动距离;
pageX,pageY:触摸点相对于document的左边缘的坐标,与clientX不同的是它包括左边滚动的距离,如果有的话;
target:总是表示手指最开始放在触摸设备上的触发点所在位置的element。
*/
/**
* 用touch事件模拟点击、左滑、右滑、上拉、下拉等事件,
* 是利用touchstart和touchend两个事件发生的位置来确定是什么操作。
* 例如:
* 1、touchstart和touchend两个事件的位置基本一致,也就是没发生位移,那么可以确定用户是想点击按钮等。
* 2、touchend在touchstart正左侧,说明用户是向左滑动的。
* 利用上面的原理,可以模拟移动端的各类事件。
**/
var EventUtil = (function() {
//支持事件列表(左滑、右滑)
var eventArr = ['eventswipeleft', 'eventswiperight'
];
//touchstart事件,delta记录开始触摸位置
function touchStart(event) {
//声明空对象,用来记录触摸开始时的位置和时间信息
this.delta = {};
//添加x坐标值
this.delta.x = event.touches[0].pageX;
//添加y坐标值
this.delta.y = event.touches[0].pageY;
}
/**
* touchend事件,计算两个事件之间的位移量
* 1、如果位移量很小或没有位移,看做点击事件
* 2、如果位移量较大,x大于y,可以看做平移,x>0,向右滑,反之向左滑。
* 这样就模拟的移动端几个常见的时间。
* */
function touchEnd(event) {
//记录开始时的位置时间信息
var delta = this.delta;
//删除开始时记录的信息
delete this.delta;
//计算坐标差值
delta.x -= event.changedTouches[0].pageX;
delta.y -= event.changedTouches[0].pageY;
// 左右滑动
if (Math.abs(delta.x) > Math.abs(delta.y)) {
//左滑
if (delta.x > 0) {
this['eventswipeleft'].map(function(fn) {
fn(event);
});
} else { //右滑
this['eventswiperight'].map(function(fn) {
fn(event);
});
}
}
} //绑定事件
function bindEvent(dom, type, callback) {
if (!dom) { //如果没有节点对象,则抛出错误
console.error('dom is null or undefined');
}
//遍历数组,检测节点对象是否绑定过事件
var flag = eventArr.some(function(key){
return dom[key]
});
//未绑定过事件
if (!flag) {
//进行绑定事件
dom.addEventListener('touchstart', touchStart);
dom.addEventListener('touchend', touchEnd);
}
//如果节点事件为空
if (!dom['event' + type]) {
//添加空数组
dom['event' + type] = [];
}
//将回调函数添加到节点事件的数组中
dom['event' + type].push(callback);
}
return {
bindEvent
}
})();
Ned
JavaScript的选择结构你真的了解吗?(看完这一篇就够了)
选择结构用于判断给定的条件,根据条件的结果来选择执行不同的语句段。实现选择结构的语句三元运算符ifswitch三元(目)运算符条件?语句:语句规则:如果条件为真,则执行?后面的语句。 如果条件为假,则执行:后面的语句。案例判断一个年份是闰年还是平年 <script>
//判断一个年份是闰年还是平年
一个年份? 当我们不知道这个数据的时候?问用户要?怎么要?
1. prompt() 类型:String 需要的是数字:parseInt() parseFloat() Number()
//判断闰年还是平年,根据条件进行判断,选择一种结果
//判断闰年的条件 1. 能被4整除但不能被100整除 2. 能被400整除
能被4整除: 余数为0 0 天然为假 我们这里需要让0为真,怎么让0为真? 1. 0 === 0 2. !0
不能被100整除: 余数不能为0 非0的数,天然为真
i_year % 4 === 0 && i_year % 100 !== 0 || i_year % 400 === 0
!(i_year % 4) && i_year % 100 || !(i_year % 400)
//准备一个变量,接收一个年份:
var i_year = parseInt(prompt('请输入一个年份:'));
alert(i_year % 4 === 0 && i_year % 100 !== 0 || i_year % 400 === 0 ? '闰年' : '平年');
</script>判断一个数是偶数还是奇数 <script>
//判断一个数是偶数还是奇数
一个数? prompt() parseInt()
偶数还是奇数?
比如有一个数4 5
// 0 假
能被2整除 ? 奇数 : 偶数
能被2整除 === 0 ? 偶数 : 奇数
能被2整除 == 0 ? 奇数 :偶数
//准备一个变量,接收一个整数:
var i = parseInt(prompt('请输入一个整数:'));
//判断
alert(i % 2 ? '奇数' : '偶数');
</script>判断一个数是正数还是负数 <script>
//判断一个数是正数还是负数
一个数? prompt() parseInt()
正数还是负数?
大于0 : 正数
小于0 : 负数
等于0 : 既不是正数,也不是负数
i === 0 ? '既不是正数,也不是负数' : i > 0 ? '正数' : '负数'
//准备一个变量,接收一个整数
var i = parseInt(prompt('请输入一个整数:'));
alert(i === 0 ? '既不是正数,也不是负数' : i > 0 ? '正数' : '负数');
</script>if 分支语句单分支选择语句if(条件){
语句组;
}流程:当程序执行到if时,先计算表达式的值,如果值为true,则执行后面大括号中的语句;如果值为false时,执行if语句后面的其它语句案例根据成绩判断是否发放清华大学的通知书? <script>
//根据成绩判断是否发放清华大学的通知书?
成绩? prompt() parseInt()
清华大学的通知书,前提条件?满足分数线? 700
成绩 >= 700
通知书
单分支
//1. 准备一个变量,接收一个成绩
var i_score = parseInt(prompt('请输入一个成绩:'));
//2. 判断成绩是否合格
if( i_score >= 700 ){
alert('祝贺你拿到清华大学的通知书!');
}
</script>输入任意两个数,然后交换位置输出(如:a=4,b=5输出a=5,b=4) <script>
//输入任意两个数,然后交换位置输出(如:a=4,b=5输出a=5,b=4)
输入任意两个数: prompt() parseInt()
交换位置: 需要一个空容器,进行交换
//1. 准备两个变量,接收两个整数:
var a = parseInt(prompt('请输入一个整数:'));
var b = parseInt(prompt('请输入一个整数:'));
//输出一次未交换的数据
console.log('交换前:\na=' + a + '\nb=' + b);
//交换位置
//准备一个空变量
var t = a;
a = b;
b = t;
//输出一次交换后的数据
console.log('交换后:\na=' + a + '\nb=' + b);
</script>输入任意三个数,由大到小输出 <script>
//输入任意三个数,由大到小输出
输入任意三个数? parInt() prompt()
大到小输出(排序):判断
两个数比较 a b c
1. 确保 a 和 b 中的最大值放到 a中
if(a < b){
交换a 和 b 中的值
}
2. 确保a 和 c 中的最大值放到 a中
if(a < c){
交换a 和 c 中的值
}
3. 确保 b 和 c 中的最大值放到 b 中
if(b < c){
交换 b 和c中的值
}
//1. 准备三个变量,接收数据
var a = parseInt(prompt('请输入一个整数:'));
var b = parseInt(prompt('请输入一个整数:'));
var c = parseInt(prompt('请输入一个整数:'));
//2. 确保 a 和 b 中的最大值放到 a中
if(a < b){
var t = a;
a = b;
b = t;
}
//3. 确保a 和 c 中的最大值放到 a中
if(a < c){
var t = a;
a = c;
c = t;
}
//4. 确保 b 和 c 中的最大值放到 b 中
if(b < c){
var t = b;
b = c;
c = t;
}
//5. 输出结果
console.log(a,b,c);
</script>双分支选择语句if(条件){
语句组;
}else{
语句组;
}流程:当程序执行到if时,先计算表达式的值,值为true时:执行if后面语句组;值为false时,执行else后的语句组。案例求两个数中的最大值? <script>
求两个数中的最大值?
两个数? prompt() parseInt()
最大值 一条件,两个结果,使用双分支
//1. 准备两个变量,接收数据
var i = parseInt(prompt('请输入一个整数:'));
var j = parseInt(prompt('请输入一个整数:'));
//2. 选择语句(双分支)
if( i > j ){
alert(i);
}else{
alert(j);
}
</script>求三个数的最大值?<script>
求三个数的最大值?
三个数? prompt() parseInt()
最大值? 两两比较最为简单 a b c
1. 求出a和b中的最大值,放到一个新的变量中。max
2. 如果c > max,则将c的值存放到max中
//1. 准备变量
var a = parseInt(prompt('请输入一个整数:'));
var b = parseInt(prompt('请输入一个整数:'));
var c = parseInt(prompt('请输入一个整数:'));
//最大值
var max;
//2. 比较前两个数,求出最大值
if(a > b){
max = a;
}else{ //否则
max = b;
}
//3. 比较下一个数与max,求出最大值
if(c > max){
max = c;
}
//4. 输出结果
alert('最大值是:' + max);
</script>输入一个成绩,判断是毕业还是挂科 <script>
输入一个成绩,判断是升班还是重修
成绩 prpmpt() parseInt()
升班还是重修? 及格分数线:90
一个条件 (成绩 >= 90) ‘毕业' '挂科' 双分支
//1. 准备一个变量
var i_score = parseInt(prompt('请输入一个成绩:'));
//2. 判断
if( i_score >= 90 ){
alert('毕业');
}else{
alert('挂科');
}
</script>多分支选择语句if(条件1){
语句组1;
}else if(条件2){
语句组2;
}
……
}else if(条件n){
语句组n;
}else{
语句组n + 1;
}流程:当程序执行到if时,先判断条件1的值,值为true时,执行语句组1;值为false时,再判断条件2的值,值为true时,执行语句组2;值为false时,再判断条件3的值,依此类推,直到判断条件n的值,值为true时,执行语句组n,值为false时,执行语句组n+1;案例任意输入一个数字,判断是星期几? <script>
任意输入一个数字,判断是星期几?
一个数字: prompt() parseInt()
1 2 3 4 5 6 7 多条件 多结果 多分支
//准备一个变量
var i = parseInt(prompt('请输入1~7中的整数:'));
//判断
if(i === 1){
alert('星期一');
}else if(i === 2){
alert('星期二');
}else if(i === 3){
alert('星期三');
}else if(i === 4){
alert('星期四');
}else if(i === 5){
alert('星期五');
}else if(i === 6){
alert('星期六');
}else if(i === 7){
alert('星期日');
}else{
alert('请输入1~7中的整数!');
}
</script>判断成绩优(90-100)良(80-89)中(70-79)差(60-69)不及格(<60) <script>
判断成绩优(90-100)良(80-89)中(70-79)差(60-69)不及格(<60)
成绩 : prompt() parseInt()
90-100 : score >= 90 && score <= 100
80-89: score >= 80 && score < 90
70-79: score >= 70 && score < 80
60-69: score >= 60 && score < 70
<60: score < 60
多分支
//1. 准备一个变量
var score = parseInt(prompt('请输入一个成绩:'));
//2. 根据成绩进行判断
if(score < 0 || score > 100){
alert('一边儿玩去!');
}else{
if(score >= 90 && score <= 100){
alert('优');
}else if(score >= 80){
alert('良');
}else if(score >= 70){
alert('中');
}else if(score >= 60){
alert('差');
}else{
alert('不及格');
}
}
</script>设计一个具有+、-、*、/、%的简单计算器 <script>
设计一个具有+、-、*、/、%的简单计算器
准备变量:3个 第一个接收数字 第二个接收运算符 第三个接收数字 prompt() parseInt()
根据运算符进行判断
+、-、*、/、% 多条件 多结果 多分支
//1. 准备变量
//1.1 第一个数字
var i = parseInt(prompt('请输入第一个整数:'));
//1.2 运算符
var ch = prompt('请输入一个算术运算符:');
//1.3 第二个数字
var j = parseInt(prompt('请输入第二个整数:'));
//2. 根据运算符进行判断
if(ch === '+'){
alert(i + '+' + j + '=' + (i + j));
}else if(ch === '-'){
alert(i + '-' + j + '=' + (i - j));
}else if(ch === '*'){
alert(i + '*' + j + '=' + i * j);
}else if(ch === '/'){
alert(j === 0 ? '除数不能为零!' : i + '/' + j + '=' + (i / j).toFixed(2));
}else if(ch === '%'){
alert(j === 0 ? '除数不能为零!' : i + '%' + j + '=' + i % j);
}else{
alert('请输入一个正确的算术运算符!');
}
</script>switch 分支语句switch(表达式){
case 表达式 : 语句组; [break;]
case 表达式 : 语句组; [break;]
……
case 表达式 : 语句组; [break;]
[default : 语句组;]
}规则:先计算switch后的表达式的值,如果这个值与 某个case后表达式的值 相同时,则执行这个case后面的语句组,如果语句组后有break,则直接跳出switch语句。如果没有break,则继续执行后面所有的语句组,直到遇到break或右大括号停止。案例任意输入一个数字,判断是星期几? <script>
任意输入一个数字,判断是星期几?
数字
7个条件,每一个条件对应一个结果
//1. 准备一个变量
var i = parseInt(prompt('请输入一个1~7中的整数:'));
//2. 根据这个整数进行判断
switch(i){
case 1 : console.log('星期一'); break;
case 2 : console.log('星期二'); break;
case 3 : console.log('星期三'); break;
case 4 : console.log('星期四'); break;
case 5 : console.log('星期五'); break;
case 6 : console.log('星期六'); break;
case 7 : console.log('星期日'); break;
default : console.log('输入错误!');
}
</script>输入一个0-6的整数,判断哪一天是工作日,哪一天是休息日? <script>
输入一个0-6的整数,判断哪一天是工作日,哪一天是休息日?
输入一个0-6的整数 prompt() parseInt()
工作日: 1 2 3 4 5
休息日: 6 7
//1. 准备一个变量
var i = parseInt(prompt('请输入0~6中的整数:'));
//2. 根据这个整数进行判断
switch(i){
case 1 :
case 2 :
case 3 :
case 4 :
case 5 : console.log('工作日'); break;
case 6 :
case 0 : console.log('休息日'); break;
default : console.log('末日'); break;
}
</script>输入一个月份,输出这个月有多少天? <script>
输入一个月份,输出这个月有多少天?
一个月份 : prompt() parseInt()
1 3 5 7 8 10 12 (31)
4 6 9 11 (30)
2 闰年(29) 平年(28)
//1. 准备一个变量
var i_month = parseInt(prompt('请输入一个月份:'));
//2. 根据月份进行判断
switch(i_month){
case 1 :
case 3 :
case 5 :
case 7 :
case 8 :
case 10 :
case 12 : alert('31天');break;
case 4 :
case 6 :
case 9 :
case 11 : alert('30天'); break;
case 2 :
//准备变量,接收年份
var i_year = parseInt(prompt('请输入一个年份:'));
alert(!(i_year % 4) && i_year % 100 || !(i_year % 400) ? '29天' : '28天');
}
/*
1. 能被4整除
0 (假) 想办法让0变为真 0 == 0 0 === 0 !0
i % 4
2. 不能被100整除
i % 100 结果是一个不为0的数 本身就是真
*/
</script>判断成绩优(90-100)良(80-89)中(70-79)差(60-69)不及格(<60) <script>
判断成绩优(90-100)良(80-89)中(70-79)差(60-69)不及格(<60)
成绩 : prompt() parseInt()
10 优
90-100 : 90 91 92 93 99 9 优
80-89: 80 81 82 83 89 8 良
70-79: 70 ~79 7 中
60-69: 60 61 69 6 差
<60:
多分支
//1. 准备一个变量
var score = parseInt(prompt('请输入一个成绩:'));
//2. 根据成绩进行判断
if(score < 0 || score > 100){
alert('一边儿玩去!');
}else{
switch(parseInt(score / 10)){
case 10 :
case 9 : alert('优'); break;
case 8 : alert('良'); break;
case 7 : alert('中'); break;
case 6 : alert('差'); break;
default : alert('不及格');
}
}
</script>计算某日是该年的第几天?<script>
计算某日是该年的第几天?
1. 需要哪些变量?
年 月 日
根据月份
从当前月份的之前月份开始加
2022、11、30
10 + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 + 日
2022、3、1
2 + 1 + 日
//1. 准备变量
//1.1 年
var year = parseInt(prompt('请输入一个年份:'));
//1.2 月
var month = parseInt(prompt('请输入一个月份:'));
//1.3 日
var date = parseInt(prompt('请输入日:'));
//1.4 总天数
var sum = 0;
//2. 根据月份判断 (从当前月的 ***前面*** 所有月份天数的和)
switch(month){ // 12月20日
case 12 : sum += 30; //(30是11月份的天数) sum = 0 + 30 30
case 11 : sum += 31; //(31是10月份的天数) sum = 30 + 31
case 10 : sum += 30; //(30是9月份的天数) sum = 61 + 30
case 9 : sum += 31; // sum = 91 + 31
case 8 : sum += 31; // sum = 122 + 31
case 7 : sum += 30; // sum = 153 + 30
case 6 : sum += 31; // sum = 183 + 31
case 5 : sum += 30; // sum = 214 + 30
case 4 : sum += 31; // sum = 244 + 31
case 3 : sum += !(year % 4) && year % 100 || !(year % 400) ? 29 : 28; // sum = 275 + 28
case 2 : sum += 31; // sum = 303 + 31
case 1 : sum += date; // sum = 334 + 20 sum = 354
}
//3. 输出结果
alert(year + '年' + month + '月' + date + '日是' + year + '年中的第' + sum + '天');
</script>
Ned
Three.js 3D创意实践指南
针对Three.js开发者和创意爱好者的专栏,提供了一系列关于使用Three.js创建令人惊叹的3D视觉效果的实用指南和技巧。从基础入门到高级创作,本专栏将带你深入了解Three.js的核心概念,并通过实例演示和踩坑日记,帮助你少走弯路,快速掌握Three.js开发的精髓。无论是制作炫酷的3D饼图、区块地图、字体效果,还是创建令人惊叹的粒子效果、词云、金字塔和地球模型,本专栏都将为你提供详细的教程和实战经验,助你在Three.js世界中展现无限创意!
Ned
JavaScript编程大师之路
在这个专栏中,我们将为您提供全面而精炼的JavaScript编程指南。从概述到选择结构、for循环、函数编写,再到数组、字符串、数学与时间处理,以及浏览器对象模型、DOM节点操作、事件绑定和传播机制等,我们将深入讲解每个主题,帮助您掌握JavaScript的核心知识和技巧。通过本专栏的学习,您将能够优雅地编写JavaScript代码,驾驭数组和字符串的神奇功能,精通Math和Date对象的使用,了解浏览器内部机制和强大的API,掌握操作元素与DOM节点的完全指南,以及实现高效可靠的事件处理。无论您是初学者还是有经验的开发者,这个专栏将成为您在JavaScript编程道路上的得力助手。
Ned
uniapp开发微信小程序--自定义头部导航栏
1. 取消页面原生的导航栏(pages.json文件)页面支持通过配置 navigationStyle为custom,或titleNView为false,来禁用原生导航栏{
"path": "pages/customer/project/index",
"style": {
"navigationBarTitleText": "服务",
//小程序
"navigationStyle": "custom",
//App、H5
"app-plus": {
"titleNView": false //禁用原生导航栏
}
}
},2. 封装navbar组件获取状态栏和导航栏的高度// 获取手机系统信息
const info = uni.getSystemInfoSync()
// 设置状态栏高度(H5顶部无状态栏小程序有状态栏需要撑起高度)
this.statusBarHeight = info.statusBarHeight
// 除了h5 app mp-alipay的情况下执行
// #ifndef H5 || APP-PLUS || MP-ALIPAY
// 获取胶囊的位置
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
// (胶囊底部高度 - 状态栏的高度) + (胶囊顶部高度 - 状态栏内的高度) = 导航栏的高度
this.navBarHeight = (menuButtonInfo.bottom - info.statusBarHeight) + (menuButtonInfo.top - info.statusBarHeight)
// #endif
2.通过props,slot在父组件自定义内容,this.$emit自定义事件 props 让组件接收外部传过来的数据
a. 传递数据:<Demo name="xxx" :age="18"/>
注:如果需要传递表达式类型的值需要使用(:属性 = "xx")的写法
b. 接收数据:
第一种方式(只接收):props:['name']
第二种方式(限制类型):props:{name:String}
第三种方式(限制类型、限制必要性、指定默认值)
props:{
name:{
type:String, //类型
required:true, //必要性
default:'老王' //默认值
}
}
slot 在子组件指定位置插入html结构,也是一种组件间通信的方式
自定义事件
a. 绑定自定义事件(父组件)@backPage="backSpace"
b. 触发自定义事件(子组件)this.$emit('backPage')
<view class="nav-box">
<view :style="{height:statusBarHeight+'px'}"></view>
<view class="navbar cor333 f34" :style="{height:navBarHeight+'px'}">
<view class="back" v-if="withBack" @tap="back">
<image src="/static/images/back-icon.png" class="back-icon"></image>
</view>
<view v-else></view>
<view class="title">{{ title }}</view>
<slot name="right"></slot>
</view>
</view>
<view :style="{height: statusBarHeight+'px'}" ></view>props: {
title: String,
withBack: Boolean
},
methods: {
back() {
this.$emit('backPage')
},
}
3.在全局引入、注册该组件import navBar from './components/navbar/index.vue'
Vue.component('nav-bar', navBar)
4.在页面中使用组件<nav-bar :title="navTitle" withBack @backPage="backSpace">
<view slot="right" class="help-icon" @tap="goHelp">使用教程</view>
</nav-bar>
data() {
return {
navTitle:'任务列表',
}
},
methods: {
backSpace(){
uni.redirectTo({
url: '../index'
})
}
}
Ned
uniapp开发微信小程序--难点总结
1. 当前页返回上一页,实现页面数据刷新在当前页面的卸载函数中,设置上一页data对象的falg数据,根据这个falg为真,来刷新页面数据//当前页
onUnload() {
let pages = getCurrentPages(); // 当前页面
let beforePage = pages[pages.length - 2]; // 上一页
beforePage.data.refreshIfNeeded = true;
},// 上一页
onShow() {
var pages = getCurrentPages(); // 获取当前页面栈
var currentPage = pages[pages.length - 1]; // 当前页面
if (currentPage.data.refreshIfNeeded) {
currentPage.data.refreshIfNeeded = false;
this.refreshMethod(); // 当前页面 method中的方法,用来刷新当前页面
}
},
methods: {
refreshMethod() {
this.getData()
},
}2. 实时展示当前年份及月份下的列表数据,切换年份或者月份实现列表数据改变1.因为月份比较多,一排占不下,所以使用swiper组件,根据current属性来控制组件的滑动。<view class="top-items cor333">
<view class="chose-year flex-align">
<view class="icon" @click="reduceyear"></view>
<view class="year f34">
<text>{{currentyear}}</text>年
</view>
<view class="icon next active" @click="addyear"></view>
</view>
<view class="chose-month f30">
<view class="page-section swiper">
<view class="page-section-spacing" >
<swiper class="swiper" display-multiple-items="6" :current="current">
<swiper-item v-for="(item,index) in month" :key="index">
<view class="swiper-item" :class="(currentmonth-1) == index? 'active': ''" @click="choseMonth(item+1)">{{item+1}}月</view>
</swiper-item>
</swiper>
</view>
</view>
</view>
</view>2.根据new Date()方法获取当前时间;getFullYear()方法获取当前年份;getMonth()方法获取当前月份,实际月份需要加1。3.当当前月份大于6时,需要控制目前滑块索引current固定在第六个(因为swiper组件控制的同时显示的滑块数量为6个,当前月份为6时,说明实际月份为7,后面7-12月就不应该再滑动了)data() {
return {
month: [0,1, 2, 3,4,5,6,7,8,9,10,11],
current: 2,
currentmonth: '',
currentyear: 2022,
};
},
created() {
const newDate = new Date();
const currentYear = newDate.getFullYear();
const currentMonth = newDate.getMonth();
this.currentyear = currentYear;
this.currentmonth = currentMonth+1;
if(currentMonth > 6){
this.current = 6
}else{
this.current = this.currentmonth - 1
}
this.initData();
},
4.滑动到最后一月,实现自动跳转下一年份;滑动到一月,实现自动跳转上一年份 首先通过@touchstart和@touchend事件确定滑动方向,触摸开始的pageX大于触摸结束的pageX,说明是向右滑动;反之向左滑动通过swiper的@animationfinish(动画结束时会触发)和@change(current 改变时会触发)事件做逻辑判断。当向右滑动,并且滑动到最后,改变当前月份、current,增加年份;当向左滑动,并且滑动到开始,改变当前月份、current,减少年份。因为第一次向右滑动到最后跳转年份正常,再次滑动到最后,current改变,但页面不再滑动到当前年份的开始位置,所以需要@change事件改变current值touchstart(e) {
this.startX = e.changedTouches[0].pageX;
},
touchend(e) {
this.endX = e.changedTouches[0].pageX;
if (this.startX > this.endX ) {
this.toRight = true
} else if(this.startX < this.endX ) {
this.toRight = false
}
},
animationfinish(e) {
this.isChange = false
let animationCurrent = e.detail.current
if (this.toRight && animationCurrent == 6) {
this.currentmonth = 1
this.current = 0
this.addyear()
// 写一个flag,swiper的change函数执行
this.isChange = true
} else if (this.toRight == false && animationCurrent == 0) {
this.currentmonth = 1
this.current = 0
this.reduceyear()
}
},
bindchange(e) {
if (this.isChange) {
this.$nextTick(() => {
this.currentmonth = 1
this.current = 0
});
}else{
// change第一次正常回到第一个之后,没有改变this.current的代码始终不执行第二次
const newDate = new Date()
const currentMonth = newDate.getMonth()
const month = currentMonth + 1
if (currentMonth > 6) {
this.current = 6
} else {
this.current = month - 1
}
}
}3. 实现图片预览previewImage(imageURL, images) {
uni.previewImage({
current: imageURL, // 当前显示图片的 http 链接
urls: images // 需要预览的图片链接列表,要求必须是数组
// loop: true // 是否可循环预览
})
}4. 列表实现展开和收起所有列表一开始都是展开的,点击不同的列表可以实现各自的展开和收起首先页面获取到数据时,给每个数据列表动态设置一个属性,控制展开和收起的状态 is_fold为真收起,为假展开this.proData.forEach(pro => {
this.$set(pro,'is_fold',false)
})2. 点击每个列表时,根据id判断是否为当前列表,改变is_fold属性,控制当前列表的状态showLists(proId){
this.proData.forEach(pro => {
if(pro.id == proId){
const value = !pro.is_fold
this.$set(pro,'is_fold',value)
}
})
},
Ned
简单实现左右内容联动(微信小程序)
之前做了一个简单的小程序项目,其中需要实现内容联动的功能。该页面左侧表示商品类别,右侧表示商品。点击左侧的商品分类菜单,右侧的内容需要滑动对应的商品分类下;滑动右侧的商品列表,左侧对应的商品分类会选中(高亮显示)。页面展示效果如下:wxml页面布局页面右侧模块需要滚动,所以使用 scroll-view组件包裹注意 : 使用scroll-view组件竖向滚动时,需要指定高度(单位为px),否则不会进行滚动 scrol-into-view的用法类似于html中#id的锚点功能,根据指向的id值(某子元素的id, 并且不能以数字开头),来决定内容滚动到哪里。<view class="catelist">
<view>
// 左侧商品类别
</view>
<scroll-view class='scrollRight' scroll-y scroll-with-animation style='height: {{winHeight}}px' bindscroll="scrollToLeft" scroll-into-view="{{contentActive}}" >
// 右侧商品列表
</scroll-view>
</view>页面功能逻辑a. 获取滚动区域高度滚动区域的高度为设备的高度,使用通过微信官方api getSystemInfo接口API获取,该api可以获取设备高度注:实际项目中,需要获取到商品数据之后,再获取高度getSystem() {
var that = this
wx.getSystemInfo({
success: function (res) {
that.setData({
winHeight: res.windowHeight
});
});
},b. 点击左侧商品分类菜单,右侧滚动到对应分类下的商品给左侧商品分类菜单绑定bindtap事件处理函数,事件触发后取出渲染页面时存放在该节点上的分类id(使用字母type加id的形式)和索引index。分类id用来作为唯一标识定位右侧商品列表,更新右侧内容scroll-view的scroll-to-view属性;索引index用来控制左侧菜单栏高亮显示。在右侧每个分类下的商品列表头部增加一个元素,加上id标识 id='type{{item.id}}'(相当于锚点),实现点击商品分类时,右侧商品列表滚动到对应分类。// 左侧点击事件
chooseType(e) {
//currentTarget表示当前绑定事件对应的节点
let dataSet = e && e.currentTarget && e.currentTarget.dataset;
this.setData({
navActive: dataSet.index,
contentActive: dataSet.id
});
},
// 左侧点击事件
chooseType(e) {
//currentTarget表示当前绑定事件对应的节点
let dataSet = e && e.currentTarget && e.currentTarget.dataset;
this.setData({
navActive: dataSet.index,
contentActive: dataSet.id
});
},
b. 滑动右侧商品列表,与之对应的左侧商品分类菜单高亮显示1.算出右侧列表中的每一个商品模块距离顶部的高度,保存至一个数组里getSystem() {
const that = this
wx.getSystemInfo({
success: function (res) {
let heightArr = [];
let h = 0;
//创建节点选择器
const query = wx.createSelectorQuery();
//选择id
query.selectAll('.list').boundingClientRect()
query.exec(function (res) {
res[0].forEach((item) => {
h += item.height;
heightArr.push(h);
})
that.setData({
top: heightArr
})
})
}
});
},
2.滑动右侧商品列表时,在bindscroll绑定的函数中,获取当前滑动的高度,与商品模块距离顶部的高度对比来知道目前滑动到那个商品区域,并返回当前分类对应的索引scrollToLeft(res) {
const scrollTop = res.detail.scrollTop;
const scorllArr = this.data.top;
const that = this;
if (scrollTop < scorllArr[scorllArr.length - 1] - that.data.winHeight) {
for (let i = 0; i < scorllArr.length; i++) {
if (scrollTop >= 0 && scrollTop < scorllArr[0]) {
that.setData({
navActive: 0,
})
} else if (scrollTop >= scorllArr[i - 1] && scrollTop < scorllArr[i]) {
that.setData({
navActive: i
})
}
}
}
},
注:此处功能暂时没有考虑左侧商品分类滑动的情况
Ned
简单实现条件筛选组件——微信小程序
前言最近做了一个微信小程序项目,列表页面有多个条件菜单项,点击条件菜单项选择相应的条件可以对列表数据进行筛选,并且有多个列表页面都需要实现条件筛选,因此封装成组件在页面中使用。条件筛选功能简单描述一共有5个条件菜单项,其中包括多级条件(该项目中最多为3级)、单个条件。点击条件菜单项,该菜单项高亮显示,并弹出该条件菜单项下的条件数据 a. 单个条件: 直接展示相应菜单项下的条件 b. 多级条件: 一开始只展示第一级的条件,点击了第一级之后,才会展示相应第二级的条件数据,第三次的条件数据依次类推点击展示出来的条件,会选中该条件,并高亮显示点击确定按钮,请求相应条件的列表数据进行展示,并关闭弹出的条件弹窗。点击重置按钮可以清除当前选中的条件效果展示(ps: 因为数据是真实的,所以处理了一下)具体实现顶部的搜索框和条件筛选模块抽离出来,封装为单独组件;条件数据可以在组件中获取,或者由外部页面传入,条件选项选中值由回调方法传到页面中使用。页面布局搜索框和条件筛选模块设置在页面顶部(相对定位);条件选项弹窗和遮罩层使用绝对定位控制显示位置;并设置条件弹窗中的条件模块的最大高度 max-height: 600rpx,使其弹窗控制在页面顶部的一定范围内,不至于占据页面整屏条件菜单项基本都是一样的,只是有一个列表页面只展示地区菜单项,因此数据是固定的,并使用一个flag值onlyDistrict控制是否显示除地区菜单项之外的菜单项(ps: 每个条件菜单项对应一个条件选项弹窗,代码太多了,这里就展示了单个条件布局)<view class='choose-list'>
<view class="container">
<view class="select-box">
<block wx:if="{{!onlyDistrict}}">
<view class="item {{industry_open? 'active' : ''}}" bindtap="openIndustry" >
行业
</view>
<view class="item {{type_open? 'active' : ''}}" bindtap="openType">
菜单2
</view>
<view class="item {{landing_open? 'active' : ''}}" bindtap="openLanding" >
菜单3
</view>
<view class="item {{cooperate_open? 'active' : ''}}" bindtap="openCooperate">
菜单4
</view>
</block>
<view class="item {{district_open? 'active' : ''}}" bindtap="openDistrict" >
地区
</view>
</view>
</view>
<!-- 单个条件-->
<view class="select-lists single-select-lists {{type_open ? 'slidup' : 'slidown'}} ">
<view class="list select-lists-center">
<view wx:for="{{typeData}}" wx:key="index" class="item {{item.is_chose? 'current': ''}}" bindtap="selectSingle" data-id="{{item.id}}" data-type="type">
<text>{{item.name}}</text>
<image src='/assets/images/chose-icon.png' class="icon {{item.is_chose?'choose':''}}"></image>
</view>
</view>
<view class='form-btn'>
<view class='btn btn-reset' bindtap='selectListsEmpty'>重置</view>
<view class='btn btn-submit' bindtap='submitType'>确定</view>
</view>
</view>
</view>
<!-- 遮罩层-->
<view class="mask {{mask? 'active' : ''}}"></view>交互处理点击条件菜单筛选栏,若当前筛选项被选中,就展示相应条件弹框,定义变量(每个条件菜单项设置一个值)控制弹框显示或隐藏openIndustry() {
if (this.data.industry_open) {
this.setData({
industry_open: false,
mask: false,
})
} else {
this.setData({
industry_open: true,
mask: true,
})
}
this.setData({
type_open: false,
landing_open: false,
cooperate_open: false,
district_open: false,
})
},单个条件选择点击某个条件选项,当前条件下其他选项取消选中,当前选项被选中,设置is_chose为true(一开始获取到的条件数据,给每一项条件都加上了is_chose字段,并设置为false)selectSingle(e) {
let id = e.currentTarget.dataset.id //当前条件选项的id
let type = e.currentTarget.dataset.type // 当前条件菜单项的类型
if (type == 'type') {
let typeData = this.data.typeData
typeData.forEach(item => {
if (item.id == id) {
item.is_chose = true
} else {
item.is_chose = false
}
})
this.setData({
typeData,
type_id: id,
})
}
},多级条件点击一级条件选项,获取当前选项下的二级条件数据,点击选中某项条件也是根据is_chose来控制selectleft(e) {
let pid = e.currentTarget.dataset.pid
let industryData = this.data.industryData
if (this.properties.cateType == 'company') {
this.getCateData({
type: 'company_industry',
pid
})
}
if (this.properties.cateType == 'project') {
this.getCateData({
type: 'project_industry',
pid
})
}
industryData.forEach(item => {
if (item.id == pid) {
item.is_chose = true
} else {
item.is_chose = false
}
})
this.setData({
industryData,
industry_id: pid
})
},
selectCenter(e) {
let id = e.currentTarget.dataset.id
let industryChildData = this.data.industryChildData
industryChildData.forEach(item => {
if (item.id == id) {
item.is_chose = true
} else {
item.is_chose = false
}
})
this.setData({
industryChildData,
industry_id: id
})
},点击重置按钮,清除当前条件数据,设为初始值点击确定按钮,使用this.triggerEvent()将条件数据传给页面,关闭弹框,在页面中调用函数请求相应条件的列表数据submitFilter() {
if (this.data.currentProvinceId) {
this.triggerEvent("selectedItem", {
province_id: this.data.currentProvinceId,
province_title: this.data.currentProvinceTitle,
city_id: this.data.currentCityId,
city_title: this.data.currentCityTitle,
area_id: this.data.currentAreaId,
})
this.setData({
district_open: false,
mask: false,
})
} else {
wx.showToast({
icon: 'none',
title: '请选择地区',
})
}
},使用组件<filter onlyDistrict="{{true}}" provinceData="{{provinceData}}" bind:selectedItem='selectedItem' />selectedItem: function (e) {
let keyword = e.detail.keyword
let province_id = e.detail.province_id
let city_id = e.detail.city_id
let area_id = e.detail.area_id
this.setData({
keyword,
province_id,
city_id,
area_id,
})
this.initData()
},
Ned
微信小程序简单实现购物车功能(三)
订单确认页面,用户信息填写操作使用微信小程序的表单组件实现,input、textarea组件都需要bindinput属性绑定事件函数,当键盘输入时,触发 input 事件,得到输入的内容event.detail.value,并改变data中相应的值。表单的验证处理,主要判断输入框是否为空值,如果为空值,使用微信自带的提示框APIwx.showToast,提示内容不能为空;像电话这样的内容还需要判断输入的号码格式是否正确,使用正则表达式进行判断。并且可以把所有需要判断的输入值,写到一个函数中,每个输入值进行判断,如果不符合条件就返回单独false,最后如果都符合了,就返回true。最后提交表单数据的时候,调用该函数进行判断。注:如果该页面会有修改的操作,那么之前填写的信息,会在进行修改的时候默认显示,那么需要把输入框的默认值value定义为之前提交数据的相应值;同时data中的相应值也需要在请求到数据(之前提交的表单数据)重新赋值,避免不修改某输入框的内容,提交的表单数据为空。<input class="weui-input chose-date" type="number" placeholder="请输入电话" value="{{orderData.phone}}" bindinput="bindTelInput" />checkForm() {
let mPattern = /^1[3456789]\d{9}$/;
if (!this.data.tel) {
wx.showToast({
icon: 'none',
title: '请输入电话!',
})
return false;
} else if (!mPattern.test(this.data.tel)) {
wx.showToast({
icon: 'none',
title: '电话格式错误',
})
return false;
}
return true;
},订单支付页面,实现微信支付调用后台的接口,将购物车的信息及填写的表单内容,传递给后台,请求成功之后,得到返回的走微信支付需要的相关参数,具体参数说明如下图:调用微信支付APIwx.requestPayment发起微信支付,在调用成功的success回调函数中,跳转到支付成功的页面。注:此项目中如果座位的总金额为0,就不需要走微信支付,因此后台不返回支付签名等参数,前端根据来判断不走微信支付toPlay() {
if (!this.data.ischose) {
wx.showToast({
icon: 'none',
title: '请勾选协议!',
})
return false;
}
let data = this.data.data
$api.scheduledSeat(data)
.then(
res => {
let order_no = res.data.order_no
if (res.data.paySign) {
wx.requestPayment({
timeStamp: res.data.timeStamp,
nonceStr: res.data.nonceStr,
package: res.data.package,
signType: res.data.signType,
paySign: res.data.paySign,
success(res) {
console.log(res)
if (res.errMsg === 'requestPayment:ok') {
wx.navigateTo({
url: '/pages/palysuccess/palysuccess?order=' + order_no,
})
}
},
fail(err) {
console.error('pay fail', err)
}
})
} else {
wx.navigateTo({
url: '/pages/palysuccess/palysuccess?order=' + order_no,
})
}
}
)
.catch(err => {
if (err.code == 202) {
wx.showToast({
icon: 'none',
title: err.msg
})
}
})
},这样,购物车的功能基本就完成了
Ned
微信小程序授权登录、授权手机号
前言最近写了一个小程序项目,原本只做用户授权登录,但是现在微信官方禁止小程序通过wx.getUserProfile 接口获取用户真正的头像和昵称,用户头像将统一返回默认灰色头像,昵称将统一返回 “微信用户”,为了区分用户,因此又做了授权获取用户手机号。总结一下实现过程以及遇到的问题。授权登录逻辑及具体实现1.调用 wx.login() 获取 临时登录凭证code(只能使用一次) ,存储到本地,并设为全局变量// 在全局app.js文件中,如果不存在用户token,就调用登录方法
login(){
// 登录
wx.login({
success: res => {
console.log(res)
// code存缓存,用于之后的请求
wx.setStorageSync('loginCode', res.code)
this.globalData.code = res.code;
}
})
},2.在需要登录的页面,点击登录按钮,通过调用接口 wx.getUserProfile 获取用户信息,该接口接口会同时返回 rawData、signature、iv、encryptedData,将这些数据和临时登录凭证code回传到开发者服务器。服务端进行验证或者注册之后返回token,表示登录成功,前端存储返回的用户信息和token授权用户信息接口返回数据的说明:// 在需要登录页面的js文件中,调用 wx.getUserProfile 接口
wx.getUserProfile({
desc: '授权登录',
success: res => {
console.log(res)
if (res.errMsg == 'getUserProfile:ok') {
// 登录信息传给后台
$api.userLogin({
code: app.globalData.code,
rawData: res.rawData,
signature: res.signature,
iv: res.iv,
encryptedData: res.encryptedData
})
.then(res => {
//请求成功,缓存用户信息,同步全局变量
...
})
.catch(err => {
//请求失败
console.log(err)
})
}
}
})3.授权手机号目前微信小程序需要用户主动触发才能发起获取手机号接口,需用 button 组件的点击来触发(新版本接口不再需要提前调用wx.login进行登录)。使用方法:将 button 组件 open-type 的值设置为 getPhoneNumber,当用户点击并同意之后,可以通过 bindgetphonenumber 事件回调获取到动态令牌code,然后把code传到开发者后台(后台调用微信后台提供的 phonenumber.getPhoneNumber 接口,消费code来换取用户手机号),返回用户手机号。<button class="loginBtn" open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">确定</button>getPhoneNumber(e) {
if (e.detail.errMsg === "getPhoneNumber:ok") {
// 同意授权绑定手机号
$api.getPhoneNumber({
code: e.detail.code,
})
.then(res => {
//请求成功
this.setData({
phone: res.data.phone,
})
})
.catch(err => {
//请求失败
})
} else if (e.detail.errMsg === "getPhoneNumber:fail user deny") {
//拒绝授权绑定手机号
}
},注:1.每个code有效期为5分钟,且只能消费一次 2.getPhoneNumber 返回的 code 与 wx.login 返回的 code 作用是不一样的,不能混用。问题:点击登录按钮,需要同时授权用户信息和手机号,因为都需要主动触发(产生点击事件)后才可调用,因此把登录的bindtap 的回调事件绑定在授权手机号的 button 组件上<button class="loginBtn" bindtap="login" open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">确定</button>
Ned
微信小程序简单实现购物车功能(一)
前言首先来简单说明下此微信小程序项目实现的购物车功能。第一步:需要先选择座位加入已选座位列表(相当于选择商品加入购物车列表),第二步:在已选座位列表页面进行座位勾选,可以全选、单选,也可以对座位进行删除,第三步:勾选好选择的座位后跳转到订单确认页面,填写用户相关信息,填写完成后,点击确定按钮跳转订单支付页面第四步:订单支付页面展示订单详细信息,勾选订座协议,走支付流程。选择座位,加入座位列表(座位可以多选)点击可选座位,座位变成被勾选的状态,再点击该座位可以取消勾选。控制座位是否选中的状态,可以在一开始请求的座位数据上加上一个是否选中的属性is_chose,一开始该属性默认为false。因为每个座位数据都是一个对象,因此可以使用Object.assign方法合并座位数据和is_chose属性组成的对象。$api.seatList(data)
.then(res => {
let tableLists = res.data.seatList
tableLists.forEach(item => {
Object.assign(item, {
is_chose: false
})
})
this.setData({
tableLists,
})
})
.catch(err => {
if (err.code == 202) {
wx.showToast({
icon: 'none',
title: err.msg
})
}
})点击选择座位,先判断座位是否已经被预订,如果没有,就循环座位数据判断每个座位的id和点击座位事件传递的id是否一致,相同的话。判断该座位的选中状态是否选中,如果选中,就改变该座位的is_chose属性为false,反之则变为true,并改变座位相关信息这里选择座位,改变座位的选中状态,然后处理座位相关的信息(像座位的号码,价格,可容纳人数等)都搞了好久,实现的时候逻辑没理清楚,就开始写了,完全是一句一句代码试出来的,太菜了,不熟悉的语法还得百度一半天。 处理座位的信息:主要是每次选中座位需要加上之前选座的信息数据,取消选中座位需要去除该座位的信息数据。如座位号,选中座位就把当前点击的座位号加上data中定义的座位号(data中的座位号数据,每点击一次,就更新一次该数据,就保留了之前点击选中的座位号);取消选中座位就把data中的座位号数据中的该座位号使用空字符串替代,就去掉了该座位号toChoseSeat(e) {
const dataset = e.currentTarget.dataset
// is_scheduled为0,表示没有被选择
if (dataset.item.is_scheduled == 0) {
let tableLists = this.data.tableLists
let tableNum;
tableLists.forEach(item => {
if (item.id == dataset.item.id) {
if (item.is_chose) {
Object.assign(item, {
is_chose: false
})
tableNum = this.data.tableNum.replace(`${dataset.item.seat_num},`, '');
} else {
Object.assign(item, {
is_chose: true
})
tableNum = this.data.tableNum + dataset.item.seat_num + ','
}
}
})
let newNumtableNum = tableNum.substring(0, tableNum.length - 1)
this.setData({
tableNum,
newNumtableNum,
tableItem: dataset.item,
tableLists,
})
}
},
选择好座位之后,点击底部的“加入已选座位”按钮,请求加入购物车接口,并将相关参数传给后台,请求接口成功后,就加入了已选座位列表,这样加入购物车的类似功能就差不多实现了
Ned
uniapp开发微信小程序--实现电子签名功能
根据uniapp的签名插件,调整封装成sign.vue组件,在页面中使用1.因为任务有保存功能,增加了image标签用于展示保存之后的签名图片。点击画布清除按钮会删除保存的签名,因此需要父组件传值签名图片url给子组件 ,子组件使用props接收,并且需要改变该值。<image class="signImg" v-if="newValue" :src="newValue" mode="widthFix"></image>props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据。此方法无法实现更改,data中的值在created生命周期或mounted生命周期里打印始终为空解决方法:通过watch来监听传递过来的字段,把这个字段赋值给data中的新字段,实现更改watch: {
value(val, oldVal) {
this.newValue = val;
},
},2.在页面初始化的时候,需要设置绘画图像的背景颜色为白色,不然图像的背景色是透明的this.ctx.setFillStyle('#fff')3.@touchmove.stop.prevent 在签名的时候,禁止页面滑动。4.签名成功后的临时存储文件路径tempFilePath为本地路径,如果直接上传该路径,后台会阻止该上传(在微信开发者工具没有问题,在真机上签名后无法提交任务),后面将签名路径(本地资源)给后台服务器(uni.uploadFile将本地资源上传到开发者服务器),再使用后台重新返回该签名新的地址提交,才成功。一直以为是 @touchmove.stop.prevent 阻止了提交按钮,用真机调试,打印一些信息,才看到后台返回的阻止信息imgUpload(tempFilePaths) {
new Promise((resolve, reject) => {
const uploadTask = uni.uploadFile({
url: this.action, //上传图片的后台接口api
filePath: tempFilePaths,
name: 'file',
fileType: 'image',
header: {
'Token': uni.getStorageSync('token'),
},
success: (uploadFileRes) => {
resolve(uploadFileRes);
this.$emit("uploadSuccess", uploadFileRes);
},
fail: (err) => {
reject(err);
this.$emit("uploadFail", err);
},
});
})
},5.在页面中使用sign.vue组件。<sign @touchmove.stop.prevent @uploadSuccess="UploadSuccess2" v-model="signUrl" :value="signUrl" />
UploadSuccess2(res) {
// 子组件传递给父组件的签名路径
var data = JSON.parse(res.data);
if (data.code == 200) {
this.signUrl = data.data
}
},
Ned
微信小程序简单实现购物车功能(二)
全选、单选、删除操作该项目的已选座位列表页面中的选择框需要自定义样式,因此没有使用微信小程序自带的 checkbox 组件。选择框有三种状态样式,默认是没有选中的状态,一种是选中状态,另一种是禁止选择的状态,这两种状态也是根据类名控制,实现方法和跟上一篇文章的类似,因此选中状态也使用了属性is_chose控制类名;禁用状态使用属性is_reserve控制,根据座位列表数据中的座位是否已经被预订来设置属性is_reserve为真。getCartData() {
$api.cartLists()
.then(res => {
let seatLists = res.data.list
seatLists.forEach(item => {
Object.assign(item, {
is_chose: false
})
item.detail.forEach(it => {
if (it.is_scheduled == 1) {
Object.assign(item, {
is_reserve: true
})
}
})
})
this.setData({
seatLists,
})
})
.catch(err => {
...
})
},a. 全选操作使用变量isall控制类名改变样式,默认值为false。点击全选按钮,判断变量isall是否为真,如果真,就改变座位列表的座位的属性is_chose为false,并且设置变量isall为假,反之则相反设置当全选按钮选中时,还需要计算所有可预订座位的价格总和,加上所有座位的id,并改变data中相应的值。取消全选,则清空座位id和总价allSelect() {
let seatLists = this.data.seatLists
if (this.data.isall) {
seatLists.forEach(item => {
Object.assign(item, {
is_chose: false
})
})
this.setData({
isall: false,
seatLists,
total_price: 0,
cartId: []
})
} else {
let total_price = 0
let cartId = []
seatLists.forEach(item => {
item.detail.forEach(it => {
if (it.is_scheduled == 0) {
Object.assign(item, {
is_chose: true
})
total_price = (parseFloat(total_price) + parseFloat(it.total_price)).toFixed(2)
cartId.push(item.id)
}
})
})
this.setData({
isall: true,
seatLists,
total_price,
cartId
})
}
},b. 单选操作首先判断通过自定义属性传递的参数reserve是否为真,为真,表示该座位已经预订,直接return出去,不能进行选择了。没有预订,就循环所有座位数据,判断每个座位的id和单选点击的id是否一致,如果一致就改变单选按钮的状态;当所有座位中有一个座位的属性is_chose为false,就取消全选,控制全选按钮的变量isall为false。同时设置一个变量num为0,当循环所有座位数据,当前座位的属性is_chose为真时,num++,在循环之后对比该变量和座位数据的长度是否相等,如果相等,控制全选按钮的变量isall为true。toSelect(e) {
let id = e.currentTarget.dataset.id;
// 购物车的座位
let is_reserve = e.currentTarget.dataset.reserve;
let seatLists = this.data.seatLists;
let num = 0;
if (is_reserve) {
wx.showToast({
icon: 'none',
title: '该座位已预订'
})
return false;
}
// 循环所有座位,如果当前id和单选点击id一致,就改变单选按钮的状态
seatLists.forEach(item => {
if (id == item.id) {
// 改变单选按钮的状态
item.is_chose = !item.is_chose
if (item.is_chose == false) {
this.setData({
isall: false,
})
}
}
if (item.is_chose) {
num++;
}
})
if (num == seatLists.length) {
this.setData({
isall: true,
})
}
this.setData({
seatLists
})
this.calculateTotal()
},c. 计算座位的总价格函数,并在每一次单选点击时调用该函数定义总价变量为0、座位id变量为空数组。循环data中的座位数据,当当前座位是选中的,并且是没有被预订,就改变总价(加上当前座位价格),座位id加上当前座位id。calculateTotal: function () {
let dataSeats = this.data.seatLists
let total_price = 0
// 勾选的座位的id
let cartId = [];
dataSeats.forEach(item => {
if (item.is_chose) {
item.detail.forEach(it => {
if (it.is_scheduled == 0) {
total_price = (parseFloat(total_price) + parseFloat(it.total_price)).toFixed(2)
cartId.push(item.id)
}
})
}
})
this.setData({
total_price,
cartId
})
},d. 删除操作可以全选座位,进行一次性删除,也可以删除每个座位数据。请求删除接口,传座位id给后台,请求成功,就删除成功了,同时删除成功后,重新请求座位数据。toDelete(e) {
// 座位id会重复(因为每个座位数据的可能包含多个座位,并且需要判断每个座位没有被预订,才加入座位id,所以会造成座位id重复),Array.from(new Set())去除重复数字
let cartId = Array.from(new Set(this.data.cartId))
if (cartId.length != 0) {
$api.delCart({
cart_id: cartId.toString()
})
.then(res => {
wx.showToast({
icon: 'none',
title: '删除成功'
})
setTimeout(() => {
this.getCartData()
}, 1000)
})
.catch(err => {
...
})
} else {
wx.showToast({
icon: 'none',
title: '请选择座位!'
})
}
},